autoglm-gui 1.5.0__py3-none-any.whl → 1.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AutoGLM_GUI/__init__.py +1 -1
- AutoGLM_GUI/__main__.py +11 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
- AutoGLM_GUI/agents/__init__.py +7 -2
- AutoGLM_GUI/agents/factory.py +46 -6
- AutoGLM_GUI/agents/glm/agent.py +8 -3
- AutoGLM_GUI/agents/glm/async_agent.py +515 -0
- AutoGLM_GUI/agents/glm/parser.py +4 -2
- AutoGLM_GUI/agents/mai/agent.py +3 -0
- AutoGLM_GUI/agents/protocols.py +111 -1
- AutoGLM_GUI/agents/stream_runner.py +11 -7
- AutoGLM_GUI/api/__init__.py +3 -1
- AutoGLM_GUI/api/agents.py +103 -37
- AutoGLM_GUI/api/devices.py +72 -0
- AutoGLM_GUI/api/history.py +27 -1
- AutoGLM_GUI/api/layered_agent.py +9 -8
- AutoGLM_GUI/api/mcp.py +6 -4
- AutoGLM_GUI/config_manager.py +38 -1
- AutoGLM_GUI/device_manager.py +28 -4
- AutoGLM_GUI/device_metadata_manager.py +174 -0
- AutoGLM_GUI/devices/mock_device.py +8 -1
- AutoGLM_GUI/models/history.py +45 -1
- AutoGLM_GUI/phone_agent_manager.py +145 -32
- AutoGLM_GUI/scheduler_manager.py +52 -6
- AutoGLM_GUI/schemas.py +101 -0
- AutoGLM_GUI/scrcpy_stream.py +2 -1
- AutoGLM_GUI/static/assets/{about-BQm96DAl.js → about-D7r9gCvG.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-B42XxGPR.js → alert-dialog-BKM-yRiQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
- AutoGLM_GUI/static/assets/{circle-alert-D4rSJh37.js → circle-alert-sohSDLhl.js} +1 -1
- AutoGLM_GUI/static/assets/{dialog-DZ78cEcj.js → dialog-BgtPh0d5.js} +1 -1
- AutoGLM_GUI/static/assets/eye-DLqKbQmg.js +1 -0
- AutoGLM_GUI/static/assets/history-Bv1lfGUU.js +1 -0
- AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
- AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
- AutoGLM_GUI/static/assets/{index-CssG-3TH.js → index-SysdKciY.js} +5 -5
- AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
- AutoGLM_GUI/static/assets/{logs-eoFxn5of.js → logs-BIhnDizW.js} +1 -1
- AutoGLM_GUI/static/assets/{popover-DLsuV5Sx.js → popover-CikYqu2P.js} +1 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-BX6y7uM5.js → textarea-knJZrz77.js} +1 -1
- AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +58 -7
- autoglm_gui-1.5.2.dist-info/RECORD +119 -0
- AutoGLM_GUI/device_adapter.py +0 -263
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +0 -129
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +0 -1
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +0 -1
- AutoGLM_GUI/static/assets/index-CmZSnDqc.js +0 -1
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +0 -1
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +0 -1
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +0 -1
- autoglm_gui-1.5.0.dist-info/RECORD +0 -157
- mai_agent/base.py +0 -137
- mai_agent/mai_grounding_agent.py +0 -263
- mai_agent/mai_naivigation_agent.py +0 -526
- mai_agent/prompt.py +0 -148
- mai_agent/unified_memory.py +0 -67
- mai_agent/utils.py +0 -73
- phone_agent/__init__.py +0 -12
- phone_agent/actions/__init__.py +0 -5
- phone_agent/actions/handler.py +0 -400
- phone_agent/actions/handler_ios.py +0 -278
- phone_agent/adb/__init__.py +0 -51
- phone_agent/adb/connection.py +0 -358
- phone_agent/adb/device.py +0 -253
- phone_agent/adb/input.py +0 -108
- phone_agent/adb/screenshot.py +0 -108
- phone_agent/agent.py +0 -253
- phone_agent/agent_ios.py +0 -277
- phone_agent/config/__init__.py +0 -53
- phone_agent/config/apps.py +0 -227
- phone_agent/config/apps_harmonyos.py +0 -256
- phone_agent/config/apps_ios.py +0 -339
- phone_agent/config/i18n.py +0 -81
- phone_agent/config/prompts.py +0 -80
- phone_agent/config/prompts_en.py +0 -79
- phone_agent/config/prompts_zh.py +0 -82
- phone_agent/config/timing.py +0 -167
- phone_agent/device_factory.py +0 -166
- phone_agent/hdc/__init__.py +0 -53
- phone_agent/hdc/connection.py +0 -384
- phone_agent/hdc/device.py +0 -269
- phone_agent/hdc/input.py +0 -145
- phone_agent/hdc/screenshot.py +0 -127
- phone_agent/model/__init__.py +0 -5
- phone_agent/model/client.py +0 -290
- phone_agent/xctest/__init__.py +0 -47
- phone_agent/xctest/connection.py +0 -379
- phone_agent/xctest/device.py +0 -472
- phone_agent/xctest/input.py +0 -311
- phone_agent/xctest/screenshot.py +0 -226
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
"""Action handler for iOS automation using WebDriverAgent."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from typing import Any, Callable
|
|
6
|
-
|
|
7
|
-
from phone_agent.xctest import (
|
|
8
|
-
back,
|
|
9
|
-
double_tap,
|
|
10
|
-
home,
|
|
11
|
-
launch_app,
|
|
12
|
-
long_press,
|
|
13
|
-
swipe,
|
|
14
|
-
tap,
|
|
15
|
-
)
|
|
16
|
-
from phone_agent.xctest.input import clear_text, hide_keyboard, type_text
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@dataclass
|
|
20
|
-
class ActionResult:
|
|
21
|
-
"""Result of an action execution."""
|
|
22
|
-
|
|
23
|
-
success: bool
|
|
24
|
-
should_finish: bool
|
|
25
|
-
message: str | None = None
|
|
26
|
-
requires_confirmation: bool = False
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class IOSActionHandler:
|
|
30
|
-
"""
|
|
31
|
-
Handles execution of actions from AI model output for iOS devices.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
wda_url: WebDriverAgent URL.
|
|
35
|
-
session_id: Optional WDA session ID.
|
|
36
|
-
confirmation_callback: Optional callback for sensitive action confirmation.
|
|
37
|
-
Should return True to proceed, False to cancel.
|
|
38
|
-
takeover_callback: Optional callback for takeover requests (login, captcha).
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self,
|
|
43
|
-
wda_url: str = "http://localhost:8100",
|
|
44
|
-
session_id: str | None = None,
|
|
45
|
-
confirmation_callback: Callable[[str], bool] | None = None,
|
|
46
|
-
takeover_callback: Callable[[str], None] | None = None,
|
|
47
|
-
):
|
|
48
|
-
self.wda_url = wda_url
|
|
49
|
-
self.session_id = session_id
|
|
50
|
-
self.confirmation_callback = confirmation_callback or self._default_confirmation
|
|
51
|
-
self.takeover_callback = takeover_callback or self._default_takeover
|
|
52
|
-
|
|
53
|
-
def execute(
|
|
54
|
-
self, action: dict[str, Any], screen_width: int, screen_height: int
|
|
55
|
-
) -> ActionResult:
|
|
56
|
-
"""
|
|
57
|
-
Execute an action from the AI model.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
action: The action dictionary from the model.
|
|
61
|
-
screen_width: Current screen width in pixels.
|
|
62
|
-
screen_height: Current screen height in pixels.
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
ActionResult indicating success and whether to finish.
|
|
66
|
-
"""
|
|
67
|
-
action_type = action.get("_metadata")
|
|
68
|
-
|
|
69
|
-
if action_type == "finish":
|
|
70
|
-
return ActionResult(
|
|
71
|
-
success=True, should_finish=True, message=action.get("message")
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if action_type != "do":
|
|
75
|
-
return ActionResult(
|
|
76
|
-
success=False,
|
|
77
|
-
should_finish=True,
|
|
78
|
-
message=f"Unknown action type: {action_type}",
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
action_name = action.get("action")
|
|
82
|
-
handler_method = self._get_handler(action_name)
|
|
83
|
-
|
|
84
|
-
if handler_method is None:
|
|
85
|
-
return ActionResult(
|
|
86
|
-
success=False,
|
|
87
|
-
should_finish=False,
|
|
88
|
-
message=f"Unknown action: {action_name}",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
return handler_method(action, screen_width, screen_height)
|
|
93
|
-
except Exception as e:
|
|
94
|
-
return ActionResult(
|
|
95
|
-
success=False, should_finish=False, message=f"Action failed: {e}"
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
def _get_handler(self, action_name: str) -> Callable | None:
|
|
99
|
-
"""Get the handler method for an action."""
|
|
100
|
-
handlers = {
|
|
101
|
-
"Launch": self._handle_launch,
|
|
102
|
-
"Tap": self._handle_tap,
|
|
103
|
-
"Type": self._handle_type,
|
|
104
|
-
"Type_Name": self._handle_type,
|
|
105
|
-
"Swipe": self._handle_swipe,
|
|
106
|
-
"Back": self._handle_back,
|
|
107
|
-
"Home": self._handle_home,
|
|
108
|
-
"Double Tap": self._handle_double_tap,
|
|
109
|
-
"Long Press": self._handle_long_press,
|
|
110
|
-
"Wait": self._handle_wait,
|
|
111
|
-
"Take_over": self._handle_takeover,
|
|
112
|
-
"Note": self._handle_note,
|
|
113
|
-
"Call_API": self._handle_call_api,
|
|
114
|
-
"Interact": self._handle_interact,
|
|
115
|
-
}
|
|
116
|
-
return handlers.get(action_name)
|
|
117
|
-
|
|
118
|
-
def _convert_relative_to_absolute(
|
|
119
|
-
self, element: list[int], screen_width: int, screen_height: int
|
|
120
|
-
) -> tuple[int, int]:
|
|
121
|
-
"""Convert relative coordinates (0-1000) to absolute pixels."""
|
|
122
|
-
x = int(element[0] / 1000 * screen_width)
|
|
123
|
-
y = int(element[1] / 1000 * screen_height)
|
|
124
|
-
return x, y
|
|
125
|
-
|
|
126
|
-
def _handle_launch(self, action: dict, width: int, height: int) -> ActionResult:
|
|
127
|
-
"""Handle app launch action."""
|
|
128
|
-
app_name = action.get("app")
|
|
129
|
-
if not app_name:
|
|
130
|
-
return ActionResult(False, False, "No app name specified")
|
|
131
|
-
|
|
132
|
-
success = launch_app(app_name, wda_url=self.wda_url, session_id=self.session_id)
|
|
133
|
-
if success:
|
|
134
|
-
return ActionResult(True, False)
|
|
135
|
-
return ActionResult(False, False, f"App not found: {app_name}")
|
|
136
|
-
|
|
137
|
-
def _handle_tap(self, action: dict, width: int, height: int) -> ActionResult:
|
|
138
|
-
"""Handle tap action."""
|
|
139
|
-
element = action.get("element")
|
|
140
|
-
if not element:
|
|
141
|
-
return ActionResult(False, False, "No element coordinates")
|
|
142
|
-
|
|
143
|
-
x, y = self._convert_relative_to_absolute(element, width, height)
|
|
144
|
-
|
|
145
|
-
print(f"Physically tap on ({x}, {y})")
|
|
146
|
-
|
|
147
|
-
# Check for sensitive operation
|
|
148
|
-
if "message" in action:
|
|
149
|
-
if not self.confirmation_callback(action["message"]):
|
|
150
|
-
return ActionResult(
|
|
151
|
-
success=False,
|
|
152
|
-
should_finish=True,
|
|
153
|
-
message="User cancelled sensitive operation",
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
tap(x, y, wda_url=self.wda_url, session_id=self.session_id)
|
|
157
|
-
return ActionResult(True, False)
|
|
158
|
-
|
|
159
|
-
def _handle_type(self, action: dict, width: int, height: int) -> ActionResult:
|
|
160
|
-
"""Handle text input action."""
|
|
161
|
-
text = action.get("text", "")
|
|
162
|
-
|
|
163
|
-
# Clear existing text and type new text
|
|
164
|
-
clear_text(wda_url=self.wda_url, session_id=self.session_id)
|
|
165
|
-
time.sleep(0.5)
|
|
166
|
-
|
|
167
|
-
type_text(text, wda_url=self.wda_url, session_id=self.session_id)
|
|
168
|
-
time.sleep(0.5)
|
|
169
|
-
|
|
170
|
-
# Hide keyboard after typing
|
|
171
|
-
hide_keyboard(wda_url=self.wda_url, session_id=self.session_id)
|
|
172
|
-
time.sleep(0.5)
|
|
173
|
-
|
|
174
|
-
return ActionResult(True, False)
|
|
175
|
-
|
|
176
|
-
def _handle_swipe(self, action: dict, width: int, height: int) -> ActionResult:
|
|
177
|
-
"""Handle swipe action."""
|
|
178
|
-
start = action.get("start")
|
|
179
|
-
end = action.get("end")
|
|
180
|
-
|
|
181
|
-
if not start or not end:
|
|
182
|
-
return ActionResult(False, False, "Missing swipe coordinates")
|
|
183
|
-
|
|
184
|
-
start_x, start_y = self._convert_relative_to_absolute(start, width, height)
|
|
185
|
-
end_x, end_y = self._convert_relative_to_absolute(end, width, height)
|
|
186
|
-
|
|
187
|
-
print(f"Physically scroll from ({start_x}, {start_y}) to ({end_x}, {end_y})")
|
|
188
|
-
|
|
189
|
-
swipe(
|
|
190
|
-
start_x,
|
|
191
|
-
start_y,
|
|
192
|
-
end_x,
|
|
193
|
-
end_y,
|
|
194
|
-
wda_url=self.wda_url,
|
|
195
|
-
session_id=self.session_id,
|
|
196
|
-
)
|
|
197
|
-
return ActionResult(True, False)
|
|
198
|
-
|
|
199
|
-
def _handle_back(self, action: dict, width: int, height: int) -> ActionResult:
|
|
200
|
-
"""Handle back gesture (swipe from left edge)."""
|
|
201
|
-
back(wda_url=self.wda_url, session_id=self.session_id)
|
|
202
|
-
return ActionResult(True, False)
|
|
203
|
-
|
|
204
|
-
def _handle_home(self, action: dict, width: int, height: int) -> ActionResult:
|
|
205
|
-
"""Handle home button action."""
|
|
206
|
-
home(wda_url=self.wda_url, session_id=self.session_id)
|
|
207
|
-
return ActionResult(True, False)
|
|
208
|
-
|
|
209
|
-
def _handle_double_tap(self, action: dict, width: int, height: int) -> ActionResult:
|
|
210
|
-
"""Handle double tap action."""
|
|
211
|
-
element = action.get("element")
|
|
212
|
-
if not element:
|
|
213
|
-
return ActionResult(False, False, "No element coordinates")
|
|
214
|
-
|
|
215
|
-
x, y = self._convert_relative_to_absolute(element, width, height)
|
|
216
|
-
double_tap(x, y, wda_url=self.wda_url, session_id=self.session_id)
|
|
217
|
-
return ActionResult(True, False)
|
|
218
|
-
|
|
219
|
-
def _handle_long_press(self, action: dict, width: int, height: int) -> ActionResult:
|
|
220
|
-
"""Handle long press action."""
|
|
221
|
-
element = action.get("element")
|
|
222
|
-
if not element:
|
|
223
|
-
return ActionResult(False, False, "No element coordinates")
|
|
224
|
-
|
|
225
|
-
x, y = self._convert_relative_to_absolute(element, width, height)
|
|
226
|
-
long_press(
|
|
227
|
-
x,
|
|
228
|
-
y,
|
|
229
|
-
duration=3.0,
|
|
230
|
-
wda_url=self.wda_url,
|
|
231
|
-
session_id=self.session_id,
|
|
232
|
-
)
|
|
233
|
-
return ActionResult(True, False)
|
|
234
|
-
|
|
235
|
-
def _handle_wait(self, action: dict, width: int, height: int) -> ActionResult:
|
|
236
|
-
"""Handle wait action."""
|
|
237
|
-
duration_str = action.get("duration", "1 seconds")
|
|
238
|
-
try:
|
|
239
|
-
duration = float(duration_str.replace("seconds", "").strip())
|
|
240
|
-
except ValueError:
|
|
241
|
-
duration = 1.0
|
|
242
|
-
|
|
243
|
-
time.sleep(duration)
|
|
244
|
-
return ActionResult(True, False)
|
|
245
|
-
|
|
246
|
-
def _handle_takeover(self, action: dict, width: int, height: int) -> ActionResult:
|
|
247
|
-
"""Handle takeover request (login, captcha, etc.)."""
|
|
248
|
-
message = action.get("message", "User intervention required")
|
|
249
|
-
self.takeover_callback(message)
|
|
250
|
-
return ActionResult(True, False)
|
|
251
|
-
|
|
252
|
-
def _handle_note(self, action: dict, width: int, height: int) -> ActionResult:
|
|
253
|
-
"""Handle note action (placeholder for content recording)."""
|
|
254
|
-
# This action is typically used for recording page content
|
|
255
|
-
# Implementation depends on specific requirements
|
|
256
|
-
return ActionResult(True, False)
|
|
257
|
-
|
|
258
|
-
def _handle_call_api(self, action: dict, width: int, height: int) -> ActionResult:
|
|
259
|
-
"""Handle API call action (placeholder for summarization)."""
|
|
260
|
-
# This action is typically used for content summarization
|
|
261
|
-
# Implementation depends on specific requirements
|
|
262
|
-
return ActionResult(True, False)
|
|
263
|
-
|
|
264
|
-
def _handle_interact(self, action: dict, width: int, height: int) -> ActionResult:
|
|
265
|
-
"""Handle interaction request (user choice needed)."""
|
|
266
|
-
# This action signals that user input is needed
|
|
267
|
-
return ActionResult(True, False, message="User interaction required")
|
|
268
|
-
|
|
269
|
-
@staticmethod
|
|
270
|
-
def _default_confirmation(message: str) -> bool:
|
|
271
|
-
"""Default confirmation callback using console input."""
|
|
272
|
-
response = input(f"Sensitive operation: {message}\nConfirm? (Y/N): ")
|
|
273
|
-
return response.upper() == "Y"
|
|
274
|
-
|
|
275
|
-
@staticmethod
|
|
276
|
-
def _default_takeover(message: str) -> None:
|
|
277
|
-
"""Default takeover callback using console input."""
|
|
278
|
-
input(f"{message}\nPress Enter after completing manual operation...")
|
phone_agent/adb/__init__.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"""ADB utilities for Android device interaction."""
|
|
2
|
-
|
|
3
|
-
from phone_agent.adb.connection import (
|
|
4
|
-
ADBConnection,
|
|
5
|
-
ConnectionType,
|
|
6
|
-
DeviceInfo,
|
|
7
|
-
list_devices,
|
|
8
|
-
quick_connect,
|
|
9
|
-
)
|
|
10
|
-
from phone_agent.adb.device import (
|
|
11
|
-
back,
|
|
12
|
-
double_tap,
|
|
13
|
-
get_current_app,
|
|
14
|
-
home,
|
|
15
|
-
launch_app,
|
|
16
|
-
long_press,
|
|
17
|
-
swipe,
|
|
18
|
-
tap,
|
|
19
|
-
)
|
|
20
|
-
from phone_agent.adb.input import (
|
|
21
|
-
clear_text,
|
|
22
|
-
detect_and_set_adb_keyboard,
|
|
23
|
-
restore_keyboard,
|
|
24
|
-
type_text,
|
|
25
|
-
)
|
|
26
|
-
from phone_agent.adb.screenshot import get_screenshot
|
|
27
|
-
|
|
28
|
-
__all__ = [
|
|
29
|
-
# Screenshot
|
|
30
|
-
"get_screenshot",
|
|
31
|
-
# Input
|
|
32
|
-
"type_text",
|
|
33
|
-
"clear_text",
|
|
34
|
-
"detect_and_set_adb_keyboard",
|
|
35
|
-
"restore_keyboard",
|
|
36
|
-
# Device control
|
|
37
|
-
"get_current_app",
|
|
38
|
-
"tap",
|
|
39
|
-
"swipe",
|
|
40
|
-
"back",
|
|
41
|
-
"home",
|
|
42
|
-
"double_tap",
|
|
43
|
-
"long_press",
|
|
44
|
-
"launch_app",
|
|
45
|
-
# Connection management
|
|
46
|
-
"ADBConnection",
|
|
47
|
-
"DeviceInfo",
|
|
48
|
-
"ConnectionType",
|
|
49
|
-
"quick_connect",
|
|
50
|
-
"list_devices",
|
|
51
|
-
]
|
phone_agent/adb/connection.py
DELETED
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
"""ADB connection management for local and remote devices."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import time
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from enum import Enum
|
|
7
|
-
|
|
8
|
-
from phone_agent.config.timing import TIMING_CONFIG
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ConnectionType(Enum):
|
|
12
|
-
"""Type of ADB connection."""
|
|
13
|
-
|
|
14
|
-
USB = "usb"
|
|
15
|
-
WIFI = "wifi"
|
|
16
|
-
REMOTE = "remote"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@dataclass
|
|
20
|
-
class DeviceInfo:
|
|
21
|
-
"""Information about a connected device."""
|
|
22
|
-
|
|
23
|
-
device_id: str
|
|
24
|
-
status: str
|
|
25
|
-
connection_type: ConnectionType
|
|
26
|
-
model: str | None = None
|
|
27
|
-
android_version: str | None = None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class ADBConnection:
|
|
31
|
-
"""
|
|
32
|
-
Manages ADB connections to Android devices.
|
|
33
|
-
|
|
34
|
-
Supports USB, WiFi, and remote TCP/IP connections.
|
|
35
|
-
|
|
36
|
-
Example:
|
|
37
|
-
>>> conn = ADBConnection()
|
|
38
|
-
>>> # Connect to remote device
|
|
39
|
-
>>> conn.connect("192.168.1.100:5555")
|
|
40
|
-
>>> # List devices
|
|
41
|
-
>>> devices = conn.list_devices()
|
|
42
|
-
>>> # Disconnect
|
|
43
|
-
>>> conn.disconnect("192.168.1.100:5555")
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(self, adb_path: str = "adb"):
|
|
47
|
-
"""
|
|
48
|
-
Initialize ADB connection manager.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
adb_path: Path to ADB executable.
|
|
52
|
-
"""
|
|
53
|
-
self.adb_path = adb_path
|
|
54
|
-
|
|
55
|
-
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
56
|
-
"""
|
|
57
|
-
Connect to a remote device via TCP/IP.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
address: Device address in format "host:port" (e.g., "192.168.1.100:5555").
|
|
61
|
-
timeout: Connection timeout in seconds.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Tuple of (success, message).
|
|
65
|
-
|
|
66
|
-
Note:
|
|
67
|
-
The remote device must have TCP/IP debugging enabled.
|
|
68
|
-
On the device, run: adb tcpip 5555
|
|
69
|
-
"""
|
|
70
|
-
# Validate address format
|
|
71
|
-
if ":" not in address:
|
|
72
|
-
address = f"{address}:5555" # Default ADB port
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
result = subprocess.run(
|
|
76
|
-
[self.adb_path, "connect", address],
|
|
77
|
-
capture_output=True,
|
|
78
|
-
text=True,
|
|
79
|
-
timeout=timeout,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
output = result.stdout + result.stderr
|
|
83
|
-
|
|
84
|
-
if "connected" in output.lower():
|
|
85
|
-
return True, f"Connected to {address}"
|
|
86
|
-
elif "already connected" in output.lower():
|
|
87
|
-
return True, f"Already connected to {address}"
|
|
88
|
-
else:
|
|
89
|
-
return False, output.strip()
|
|
90
|
-
|
|
91
|
-
except subprocess.TimeoutExpired:
|
|
92
|
-
return False, f"Connection timeout after {timeout}s"
|
|
93
|
-
except Exception as e:
|
|
94
|
-
return False, f"Connection error: {e}"
|
|
95
|
-
|
|
96
|
-
def disconnect(self, address: str | None = None) -> tuple[bool, str]:
|
|
97
|
-
"""
|
|
98
|
-
Disconnect from a remote device.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
address: Device address to disconnect. If None, disconnects all.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
Tuple of (success, message).
|
|
105
|
-
"""
|
|
106
|
-
try:
|
|
107
|
-
cmd = [self.adb_path, "disconnect"]
|
|
108
|
-
if address:
|
|
109
|
-
cmd.append(address)
|
|
110
|
-
|
|
111
|
-
result = subprocess.run(
|
|
112
|
-
cmd, capture_output=True, text=True, encoding="utf-8", timeout=5
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
output = result.stdout + result.stderr
|
|
116
|
-
return True, output.strip() or "Disconnected"
|
|
117
|
-
|
|
118
|
-
except Exception as e:
|
|
119
|
-
return False, f"Disconnect error: {e}"
|
|
120
|
-
|
|
121
|
-
def list_devices(self) -> list[DeviceInfo]:
|
|
122
|
-
"""
|
|
123
|
-
List all connected devices.
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
List of DeviceInfo objects.
|
|
127
|
-
"""
|
|
128
|
-
try:
|
|
129
|
-
result = subprocess.run(
|
|
130
|
-
[self.adb_path, "devices", "-l"],
|
|
131
|
-
capture_output=True,
|
|
132
|
-
text=True,
|
|
133
|
-
timeout=5,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
devices = []
|
|
137
|
-
for line in result.stdout.strip().split("\n")[1:]: # Skip header
|
|
138
|
-
if not line.strip():
|
|
139
|
-
continue
|
|
140
|
-
|
|
141
|
-
parts = line.split()
|
|
142
|
-
if len(parts) >= 2:
|
|
143
|
-
device_id = parts[0]
|
|
144
|
-
status = parts[1]
|
|
145
|
-
|
|
146
|
-
# Determine connection type
|
|
147
|
-
if ":" in device_id:
|
|
148
|
-
conn_type = ConnectionType.REMOTE
|
|
149
|
-
elif "emulator" in device_id:
|
|
150
|
-
conn_type = ConnectionType.USB # Emulator via USB
|
|
151
|
-
else:
|
|
152
|
-
conn_type = ConnectionType.USB
|
|
153
|
-
|
|
154
|
-
# Parse additional info
|
|
155
|
-
model = None
|
|
156
|
-
for part in parts[2:]:
|
|
157
|
-
if part.startswith("model:"):
|
|
158
|
-
model = part.split(":", 1)[1]
|
|
159
|
-
break
|
|
160
|
-
|
|
161
|
-
devices.append(
|
|
162
|
-
DeviceInfo(
|
|
163
|
-
device_id=device_id,
|
|
164
|
-
status=status,
|
|
165
|
-
connection_type=conn_type,
|
|
166
|
-
model=model,
|
|
167
|
-
)
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
return devices
|
|
171
|
-
|
|
172
|
-
except Exception as e:
|
|
173
|
-
print(f"Error listing devices: {e}")
|
|
174
|
-
return []
|
|
175
|
-
|
|
176
|
-
def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
|
|
177
|
-
"""
|
|
178
|
-
Get detailed information about a device.
|
|
179
|
-
|
|
180
|
-
Args:
|
|
181
|
-
device_id: Device ID. If None, uses first available device.
|
|
182
|
-
|
|
183
|
-
Returns:
|
|
184
|
-
DeviceInfo or None if not found.
|
|
185
|
-
"""
|
|
186
|
-
devices = self.list_devices()
|
|
187
|
-
|
|
188
|
-
if not devices:
|
|
189
|
-
return None
|
|
190
|
-
|
|
191
|
-
if device_id is None:
|
|
192
|
-
return devices[0]
|
|
193
|
-
|
|
194
|
-
for device in devices:
|
|
195
|
-
if device.device_id == device_id:
|
|
196
|
-
return device
|
|
197
|
-
|
|
198
|
-
return None
|
|
199
|
-
|
|
200
|
-
def is_connected(self, device_id: str | None = None) -> bool:
|
|
201
|
-
"""
|
|
202
|
-
Check if a device is connected.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
device_id: Device ID to check. If None, checks if any device is connected.
|
|
206
|
-
|
|
207
|
-
Returns:
|
|
208
|
-
True if connected, False otherwise.
|
|
209
|
-
"""
|
|
210
|
-
devices = self.list_devices()
|
|
211
|
-
|
|
212
|
-
if not devices:
|
|
213
|
-
return False
|
|
214
|
-
|
|
215
|
-
if device_id is None:
|
|
216
|
-
return any(d.status == "device" for d in devices)
|
|
217
|
-
|
|
218
|
-
return any(d.device_id == device_id and d.status == "device" for d in devices)
|
|
219
|
-
|
|
220
|
-
def enable_tcpip(
|
|
221
|
-
self, port: int = 5555, device_id: str | None = None
|
|
222
|
-
) -> tuple[bool, str]:
|
|
223
|
-
"""
|
|
224
|
-
Enable TCP/IP debugging on a USB-connected device.
|
|
225
|
-
|
|
226
|
-
This allows subsequent wireless connections to the device.
|
|
227
|
-
|
|
228
|
-
Args:
|
|
229
|
-
port: TCP port for ADB (default: 5555).
|
|
230
|
-
device_id: Device ID. If None, uses first available device.
|
|
231
|
-
|
|
232
|
-
Returns:
|
|
233
|
-
Tuple of (success, message).
|
|
234
|
-
|
|
235
|
-
Note:
|
|
236
|
-
The device must be connected via USB first.
|
|
237
|
-
After this, you can disconnect USB and connect via WiFi.
|
|
238
|
-
"""
|
|
239
|
-
try:
|
|
240
|
-
cmd = [self.adb_path]
|
|
241
|
-
if device_id:
|
|
242
|
-
cmd.extend(["-s", device_id])
|
|
243
|
-
cmd.extend(["tcpip", str(port)])
|
|
244
|
-
|
|
245
|
-
result = subprocess.run(
|
|
246
|
-
cmd, capture_output=True, text=True, encoding="utf-8", timeout=10
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
output = result.stdout + result.stderr
|
|
250
|
-
|
|
251
|
-
if "restarting" in output.lower() or result.returncode == 0:
|
|
252
|
-
time.sleep(TIMING_CONFIG.connection.adb_restart_delay)
|
|
253
|
-
return True, f"TCP/IP mode enabled on port {port}"
|
|
254
|
-
else:
|
|
255
|
-
return False, output.strip()
|
|
256
|
-
|
|
257
|
-
except Exception as e:
|
|
258
|
-
return False, f"Error enabling TCP/IP: {e}"
|
|
259
|
-
|
|
260
|
-
def get_device_ip(self, device_id: str | None = None) -> str | None:
|
|
261
|
-
"""
|
|
262
|
-
Get the IP address of a connected device.
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
device_id: Device ID. If None, uses first available device.
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
IP address string or None if not found.
|
|
269
|
-
"""
|
|
270
|
-
try:
|
|
271
|
-
cmd = [self.adb_path]
|
|
272
|
-
if device_id:
|
|
273
|
-
cmd.extend(["-s", device_id])
|
|
274
|
-
cmd.extend(["shell", "ip", "route"])
|
|
275
|
-
|
|
276
|
-
result = subprocess.run(
|
|
277
|
-
cmd, capture_output=True, text=True, encoding="utf-8", timeout=5
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
# Parse IP from route output
|
|
281
|
-
for line in result.stdout.split("\n"):
|
|
282
|
-
if "src" in line:
|
|
283
|
-
parts = line.split()
|
|
284
|
-
for i, part in enumerate(parts):
|
|
285
|
-
if part == "src" and i + 1 < len(parts):
|
|
286
|
-
return parts[i + 1]
|
|
287
|
-
|
|
288
|
-
# Alternative: try wlan0 interface
|
|
289
|
-
cmd[-1] = "ip addr show wlan0"
|
|
290
|
-
result = subprocess.run(
|
|
291
|
-
cmd[:-1] + ["shell", "ip", "addr", "show", "wlan0"],
|
|
292
|
-
capture_output=True,
|
|
293
|
-
text=True,
|
|
294
|
-
encoding="utf-8",
|
|
295
|
-
timeout=5,
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
for line in result.stdout.split("\n"):
|
|
299
|
-
if "inet " in line:
|
|
300
|
-
parts = line.strip().split()
|
|
301
|
-
if len(parts) >= 2:
|
|
302
|
-
return parts[1].split("/")[0]
|
|
303
|
-
|
|
304
|
-
return None
|
|
305
|
-
|
|
306
|
-
except Exception as e:
|
|
307
|
-
print(f"Error getting device IP: {e}")
|
|
308
|
-
return None
|
|
309
|
-
|
|
310
|
-
def restart_server(self) -> tuple[bool, str]:
|
|
311
|
-
"""
|
|
312
|
-
Restart the ADB server.
|
|
313
|
-
|
|
314
|
-
Returns:
|
|
315
|
-
Tuple of (success, message).
|
|
316
|
-
"""
|
|
317
|
-
try:
|
|
318
|
-
# Kill server
|
|
319
|
-
subprocess.run(
|
|
320
|
-
[self.adb_path, "kill-server"], capture_output=True, timeout=5
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
time.sleep(TIMING_CONFIG.connection.server_restart_delay)
|
|
324
|
-
|
|
325
|
-
# Start server
|
|
326
|
-
subprocess.run(
|
|
327
|
-
[self.adb_path, "start-server"], capture_output=True, timeout=5
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
return True, "ADB server restarted"
|
|
331
|
-
|
|
332
|
-
except Exception as e:
|
|
333
|
-
return False, f"Error restarting server: {e}"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def quick_connect(address: str) -> tuple[bool, str]:
|
|
337
|
-
"""
|
|
338
|
-
Quick helper to connect to a remote device.
|
|
339
|
-
|
|
340
|
-
Args:
|
|
341
|
-
address: Device address (e.g., "192.168.1.100" or "192.168.1.100:5555").
|
|
342
|
-
|
|
343
|
-
Returns:
|
|
344
|
-
Tuple of (success, message).
|
|
345
|
-
"""
|
|
346
|
-
conn = ADBConnection()
|
|
347
|
-
return conn.connect(address)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def list_devices() -> list[DeviceInfo]:
|
|
351
|
-
"""
|
|
352
|
-
Quick helper to list connected devices.
|
|
353
|
-
|
|
354
|
-
Returns:
|
|
355
|
-
List of DeviceInfo objects.
|
|
356
|
-
"""
|
|
357
|
-
conn = ADBConnection()
|
|
358
|
-
return conn.list_devices()
|