autoglm-gui 1.4.0__py3-none-any.whl → 1.5.0__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 +11 -0
- AutoGLM_GUI/__main__.py +26 -8
- AutoGLM_GUI/actions/__init__.py +6 -0
- AutoGLM_GUI/actions/handler.py +196 -0
- AutoGLM_GUI/actions/types.py +15 -0
- AutoGLM_GUI/adb/__init__.py +53 -0
- AutoGLM_GUI/adb/apps.py +227 -0
- AutoGLM_GUI/adb/connection.py +323 -0
- AutoGLM_GUI/adb/device.py +171 -0
- AutoGLM_GUI/adb/input.py +67 -0
- AutoGLM_GUI/adb/screenshot.py +11 -0
- AutoGLM_GUI/adb/timing.py +167 -0
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +51 -0
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +153 -0
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +292 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
- AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +405 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +27 -0
- AutoGLM_GUI/agents/stream_runner.py +188 -0
- AutoGLM_GUI/api/__init__.py +71 -11
- AutoGLM_GUI/api/agents.py +190 -229
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +112 -28
- AutoGLM_GUI/api/health.py +13 -0
- AutoGLM_GUI/api/history.py +78 -0
- AutoGLM_GUI/api/layered_agent.py +306 -181
- AutoGLM_GUI/api/mcp.py +11 -10
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/api/version.py +23 -10
- AutoGLM_GUI/api/workflows.py +2 -1
- AutoGLM_GUI/config.py +72 -14
- AutoGLM_GUI/config_manager.py +98 -27
- AutoGLM_GUI/device_adapter.py +263 -0
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +266 -0
- AutoGLM_GUI/devices/__init__.py +49 -0
- AutoGLM_GUI/devices/adb_device.py +200 -0
- AutoGLM_GUI/devices/mock_device.py +185 -0
- AutoGLM_GUI/devices/remote_device.py +177 -0
- AutoGLM_GUI/exceptions.py +3 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/i18n.py +81 -0
- AutoGLM_GUI/metrics.py +13 -20
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +96 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +118 -367
- AutoGLM_GUI/platform_utils.py +31 -2
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +304 -0
- AutoGLM_GUI/schemas.py +272 -63
- AutoGLM_GUI/scrcpy_stream.py +159 -37
- AutoGLM_GUI/server.py +3 -1
- AutoGLM_GUI/socketio_server.py +114 -29
- AutoGLM_GUI/state.py +10 -30
- AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
- AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
- AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
- AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
- AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
- AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
- AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +142 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- mai_agent/base.py +137 -0
- mai_agent/mai_grounding_agent.py +263 -0
- mai_agent/mai_naivigation_agent.py +526 -0
- mai_agent/prompt.py +148 -0
- mai_agent/unified_memory.py +67 -0
- mai_agent/utils.py +73 -0
- AutoGLM_GUI/api/dual_model.py +0 -311
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -146
- AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
- AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
- AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
- AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
- AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
- autoglm_gui-1.4.0.dist-info/RECORD +0 -100
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Device Protocol - Abstract interface for device operations.
|
|
2
|
+
|
|
3
|
+
This module defines the protocol (interface) that all device implementations
|
|
4
|
+
must follow. The actual implementation can be:
|
|
5
|
+
- ADB (local subprocess calls)
|
|
6
|
+
- Accessibility Service
|
|
7
|
+
- Remote HTTP/gRPC calls
|
|
8
|
+
- Mock (for testing)
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from AutoGLM_GUI.devices import ADBDevice, MockDevice
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Production: use ADB
|
|
14
|
+
>>> device = ADBDevice("emulator-5554")
|
|
15
|
+
>>> screenshot = device.get_screenshot()
|
|
16
|
+
>>> device.tap(100, 200)
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Testing: use Mock with state machine
|
|
19
|
+
>>> mock = MockDevice("mock_001", state_machine)
|
|
20
|
+
>>> screenshot = mock.get_screenshot() # Returns state machine's screenshot
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Screenshot:
|
|
29
|
+
"""Screenshot result from device."""
|
|
30
|
+
|
|
31
|
+
base64_data: str
|
|
32
|
+
width: int
|
|
33
|
+
height: int
|
|
34
|
+
is_sensitive: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DeviceInfo:
|
|
39
|
+
"""Information about a connected device."""
|
|
40
|
+
|
|
41
|
+
device_id: str
|
|
42
|
+
status: str # "online" | "offline" | "unauthorized"
|
|
43
|
+
model: str | None = None
|
|
44
|
+
platform: str = "android" # "android" | "ios" | "harmonyos"
|
|
45
|
+
connection_type: str = "usb" # "usb" | "wifi" (ADB WiFi) | "remote" (HTTP Remote)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class DeviceProtocol(Protocol):
|
|
50
|
+
"""
|
|
51
|
+
Device operation protocol - all device implementations must follow this interface.
|
|
52
|
+
|
|
53
|
+
This protocol abstracts device operations, allowing the control logic to be
|
|
54
|
+
independent of the actual device implementation (ADB, Accessibility, Remote, etc.).
|
|
55
|
+
|
|
56
|
+
The concrete implementation decides HOW to perform operations:
|
|
57
|
+
- ADBDevice: Uses `adb shell input tap` commands
|
|
58
|
+
- AccessibilityDevice: Uses Android Accessibility Service
|
|
59
|
+
- RemoteDevice: Sends HTTP/gRPC requests to a remote agent
|
|
60
|
+
- MockDevice: Routes operations through a state machine for testing
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def device_id(self) -> str:
|
|
65
|
+
"""Unique device identifier."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# === Screenshot ===
|
|
69
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
70
|
+
"""
|
|
71
|
+
Capture current screen.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
timeout: Timeout in seconds for the operation.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Screenshot object containing base64 data and dimensions.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
# === Input Operations ===
|
|
82
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Tap at specified coordinates.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
x: X coordinate.
|
|
88
|
+
y: Y coordinate.
|
|
89
|
+
delay: Optional delay after tap in seconds.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Double tap at specified coordinates.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
x: X coordinate.
|
|
99
|
+
y: Y coordinate.
|
|
100
|
+
delay: Optional delay after double tap in seconds.
|
|
101
|
+
"""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def long_press(
|
|
105
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Long press at specified coordinates.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
x: X coordinate.
|
|
112
|
+
y: Y coordinate.
|
|
113
|
+
duration_ms: Duration of press in milliseconds.
|
|
114
|
+
delay: Optional delay after long press in seconds.
|
|
115
|
+
"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
def swipe(
|
|
119
|
+
self,
|
|
120
|
+
start_x: int,
|
|
121
|
+
start_y: int,
|
|
122
|
+
end_x: int,
|
|
123
|
+
end_y: int,
|
|
124
|
+
duration_ms: int | None = None,
|
|
125
|
+
delay: float | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Swipe from start to end coordinates.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
start_x: Starting X coordinate.
|
|
132
|
+
start_y: Starting Y coordinate.
|
|
133
|
+
end_x: Ending X coordinate.
|
|
134
|
+
end_y: Ending Y coordinate.
|
|
135
|
+
duration_ms: Duration of swipe in milliseconds.
|
|
136
|
+
delay: Optional delay after swipe in seconds.
|
|
137
|
+
"""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
def type_text(self, text: str) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Type text into the currently focused input field.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
text: The text to type.
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def clear_text(self) -> None:
|
|
150
|
+
"""Clear text in the currently focused input field."""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
# === Navigation ===
|
|
154
|
+
def back(self, delay: float | None = None) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Press the back button.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
delay: Optional delay after pressing back in seconds.
|
|
160
|
+
"""
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
def home(self, delay: float | None = None) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Press the home button.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
delay: Optional delay after pressing home in seconds.
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Launch an app by name.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
app_name: The app name to launch.
|
|
178
|
+
delay: Optional delay after launching in seconds.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if app was launched successfully, False otherwise.
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
# === State Query ===
|
|
186
|
+
def get_current_app(self) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Get the currently focused app name.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The app name if recognized, otherwise "System Home".
|
|
192
|
+
"""
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
# === Keyboard Management ===
|
|
196
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Detect current keyboard and switch to ADB Keyboard if needed.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The original keyboard IME identifier for later restoration.
|
|
202
|
+
"""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Restore the original keyboard IME.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
ime: The IME identifier to restore.
|
|
211
|
+
"""
|
|
212
|
+
...
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@runtime_checkable
|
|
216
|
+
class DeviceManagerProtocol(Protocol):
|
|
217
|
+
"""Device manager protocol - manages multiple devices."""
|
|
218
|
+
|
|
219
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
220
|
+
"""
|
|
221
|
+
List all available devices.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of DeviceInfo objects.
|
|
225
|
+
"""
|
|
226
|
+
...
|
|
227
|
+
|
|
228
|
+
def get_device(self, device_id: str) -> DeviceProtocol:
|
|
229
|
+
"""
|
|
230
|
+
Get a device instance by ID.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
device_id: The device ID.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
DeviceProtocol implementation for the device.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
KeyError: If device not found.
|
|
240
|
+
"""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
244
|
+
"""
|
|
245
|
+
Connect to a remote device.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
address: Device address (e.g., "192.168.1.100:5555").
|
|
249
|
+
timeout: Connection timeout in seconds.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Tuple of (success, message).
|
|
253
|
+
"""
|
|
254
|
+
...
|
|
255
|
+
|
|
256
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
257
|
+
"""
|
|
258
|
+
Disconnect from a device.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
device_id: The device ID to disconnect.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Tuple of (success, message).
|
|
265
|
+
"""
|
|
266
|
+
...
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Device implementations for the DeviceProtocol interface.
|
|
2
|
+
|
|
3
|
+
This package provides concrete implementations of DeviceProtocol:
|
|
4
|
+
- ADBDevice: Local ADB subprocess calls
|
|
5
|
+
- MockDevice: State machine driven mock for testing
|
|
6
|
+
- RemoteDevice: HTTP client for remote device agents
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from AutoGLM_GUI.devices import ADBDevice, RemoteDevice, get_device_manager
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Local ADB device
|
|
12
|
+
>>> device = ADBDevice("emulator-5554")
|
|
13
|
+
>>> device.tap(100, 200)
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Remote device via HTTP
|
|
16
|
+
>>> remote = RemoteDevice("phone_001", "http://device-agent:8001")
|
|
17
|
+
>>> remote.tap(100, 200)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from AutoGLM_GUI.devices.adb_device import ADBDevice, ADBDeviceManager
|
|
21
|
+
from AutoGLM_GUI.devices.mock_device import MockDevice
|
|
22
|
+
from AutoGLM_GUI.devices.remote_device import RemoteDevice, RemoteDeviceManager
|
|
23
|
+
|
|
24
|
+
_device_manager: "ADBDeviceManager | None" = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_device_manager() -> ADBDeviceManager:
|
|
28
|
+
"""Get the global device manager instance."""
|
|
29
|
+
global _device_manager
|
|
30
|
+
if _device_manager is None:
|
|
31
|
+
_device_manager = ADBDeviceManager()
|
|
32
|
+
return _device_manager
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_device_manager(manager: "ADBDeviceManager") -> None:
|
|
36
|
+
"""Set the global device manager instance (useful for testing)."""
|
|
37
|
+
global _device_manager
|
|
38
|
+
_device_manager = manager
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ADBDevice",
|
|
43
|
+
"ADBDeviceManager",
|
|
44
|
+
"MockDevice",
|
|
45
|
+
"RemoteDevice",
|
|
46
|
+
"RemoteDeviceManager",
|
|
47
|
+
"get_device_manager",
|
|
48
|
+
"set_device_manager",
|
|
49
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""ADB Device implementation of DeviceProtocol."""
|
|
2
|
+
|
|
3
|
+
from AutoGLM_GUI import adb
|
|
4
|
+
from AutoGLM_GUI.adb import ADBConnection
|
|
5
|
+
from AutoGLM_GUI.device_protocol import (
|
|
6
|
+
DeviceInfo,
|
|
7
|
+
DeviceManagerProtocol,
|
|
8
|
+
DeviceProtocol,
|
|
9
|
+
Screenshot,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ADBDevice(DeviceProtocol):
|
|
14
|
+
"""
|
|
15
|
+
ADB device implementation using local subprocess calls.
|
|
16
|
+
|
|
17
|
+
Wraps the existing phone_agent.adb module to provide a clean
|
|
18
|
+
DeviceProtocol interface.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> device = ADBDevice("emulator-5554")
|
|
22
|
+
>>> screenshot = device.get_screenshot()
|
|
23
|
+
>>> device.tap(100, 200)
|
|
24
|
+
>>> device.swipe(100, 200, 300, 400)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, device_id: str):
|
|
28
|
+
"""
|
|
29
|
+
Initialize ADB device.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
device_id: ADB device ID (e.g., "emulator-5554", "192.168.1.100:5555").
|
|
33
|
+
"""
|
|
34
|
+
self._device_id = device_id
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def device_id(self) -> str:
|
|
38
|
+
"""Unique device identifier."""
|
|
39
|
+
return self._device_id
|
|
40
|
+
|
|
41
|
+
# === Screenshot ===
|
|
42
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
43
|
+
"""Capture current screen."""
|
|
44
|
+
result = adb.get_screenshot(self._device_id, timeout)
|
|
45
|
+
return Screenshot(
|
|
46
|
+
base64_data=result.base64_data,
|
|
47
|
+
width=result.width,
|
|
48
|
+
height=result.height,
|
|
49
|
+
is_sensitive=result.is_sensitive,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# === Input Operations ===
|
|
53
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
54
|
+
"""Tap at specified coordinates."""
|
|
55
|
+
adb.tap(x, y, self._device_id, delay)
|
|
56
|
+
|
|
57
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
58
|
+
"""Double tap at specified coordinates."""
|
|
59
|
+
adb.double_tap(x, y, self._device_id, delay)
|
|
60
|
+
|
|
61
|
+
def long_press(
|
|
62
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Long press at specified coordinates."""
|
|
65
|
+
adb.long_press(x, y, duration_ms, self._device_id, delay)
|
|
66
|
+
|
|
67
|
+
def swipe(
|
|
68
|
+
self,
|
|
69
|
+
start_x: int,
|
|
70
|
+
start_y: int,
|
|
71
|
+
end_x: int,
|
|
72
|
+
end_y: int,
|
|
73
|
+
duration_ms: int | None = None,
|
|
74
|
+
delay: float | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Swipe from start to end coordinates."""
|
|
77
|
+
adb.swipe(start_x, start_y, end_x, end_y, duration_ms, self._device_id, delay)
|
|
78
|
+
|
|
79
|
+
def type_text(self, text: str) -> None:
|
|
80
|
+
"""Type text into the currently focused input field."""
|
|
81
|
+
adb.type_text(text, self._device_id)
|
|
82
|
+
|
|
83
|
+
def clear_text(self) -> None:
|
|
84
|
+
"""Clear text in the currently focused input field."""
|
|
85
|
+
adb.clear_text(self._device_id)
|
|
86
|
+
|
|
87
|
+
# === Navigation ===
|
|
88
|
+
def back(self, delay: float | None = None) -> None:
|
|
89
|
+
"""Press the back button."""
|
|
90
|
+
adb.back(self._device_id, delay)
|
|
91
|
+
|
|
92
|
+
def home(self, delay: float | None = None) -> None:
|
|
93
|
+
"""Press the home button."""
|
|
94
|
+
adb.home(self._device_id, delay)
|
|
95
|
+
|
|
96
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
97
|
+
"""Launch an app by name."""
|
|
98
|
+
return adb.launch_app(app_name, self._device_id, delay)
|
|
99
|
+
|
|
100
|
+
# === State Query ===
|
|
101
|
+
def get_current_app(self) -> str:
|
|
102
|
+
"""Get the currently focused app name."""
|
|
103
|
+
return adb.get_current_app(self._device_id)
|
|
104
|
+
|
|
105
|
+
# === Keyboard Management ===
|
|
106
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
107
|
+
"""Detect current keyboard and switch to ADB Keyboard if needed."""
|
|
108
|
+
return adb.detect_and_set_adb_keyboard(self._device_id)
|
|
109
|
+
|
|
110
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
111
|
+
"""Restore the original keyboard IME."""
|
|
112
|
+
adb.restore_keyboard(ime, self._device_id)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Verify ADBDevice implements DeviceProtocol
|
|
116
|
+
assert isinstance(ADBDevice("test"), DeviceProtocol)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ADBDeviceManager(DeviceManagerProtocol):
|
|
120
|
+
"""
|
|
121
|
+
ADB device manager implementation.
|
|
122
|
+
|
|
123
|
+
Manages multiple ADB devices and provides DeviceProtocol instances.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> manager = ADBDeviceManager()
|
|
127
|
+
>>> devices = manager.list_devices()
|
|
128
|
+
>>> device = manager.get_device("emulator-5554")
|
|
129
|
+
>>> device.tap(100, 200)
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, adb_path: str = "adb"):
|
|
133
|
+
"""
|
|
134
|
+
Initialize the device manager.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
adb_path: Path to ADB executable.
|
|
138
|
+
"""
|
|
139
|
+
self._connection = ADBConnection(adb_path)
|
|
140
|
+
self._devices: dict[str, ADBDevice] = {}
|
|
141
|
+
|
|
142
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
143
|
+
"""List all available devices."""
|
|
144
|
+
adb_devices = self._connection.list_devices()
|
|
145
|
+
result = []
|
|
146
|
+
|
|
147
|
+
for dev in adb_devices:
|
|
148
|
+
result.append(
|
|
149
|
+
DeviceInfo(
|
|
150
|
+
device_id=dev.device_id,
|
|
151
|
+
status="online" if dev.status == "device" else dev.status,
|
|
152
|
+
model=dev.model,
|
|
153
|
+
platform="android",
|
|
154
|
+
connection_type=dev.connection_type.value,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
def get_device(self, device_id: str) -> ADBDevice:
|
|
161
|
+
"""
|
|
162
|
+
Get a device instance by ID.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
device_id: The device ID.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ADBDevice instance.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
KeyError: If device not found or offline.
|
|
172
|
+
"""
|
|
173
|
+
# Check if device exists and is online
|
|
174
|
+
devices = self.list_devices()
|
|
175
|
+
device_info = next((d for d in devices if d.device_id == device_id), None)
|
|
176
|
+
|
|
177
|
+
if device_info is None:
|
|
178
|
+
raise KeyError(f"Device '{device_id}' not found")
|
|
179
|
+
if device_info.status != "online":
|
|
180
|
+
raise KeyError(f"Device '{device_id}' is {device_info.status}")
|
|
181
|
+
|
|
182
|
+
# Cache and return device instance
|
|
183
|
+
if device_id not in self._devices:
|
|
184
|
+
self._devices[device_id] = ADBDevice(device_id)
|
|
185
|
+
|
|
186
|
+
return self._devices[device_id]
|
|
187
|
+
|
|
188
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
189
|
+
"""Connect to a remote device."""
|
|
190
|
+
return self._connection.connect(address, timeout)
|
|
191
|
+
|
|
192
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
193
|
+
"""Disconnect from a device."""
|
|
194
|
+
# Remove from cache
|
|
195
|
+
self._devices.pop(device_id, None)
|
|
196
|
+
return self._connection.disconnect(device_id)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Verify ADBDeviceManager implements DeviceManagerProtocol
|
|
200
|
+
assert isinstance(ADBDeviceManager(), DeviceManagerProtocol)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Mock Device implementation for testing.
|
|
2
|
+
|
|
3
|
+
This module provides a MockDevice that routes all operations through
|
|
4
|
+
a state machine, enabling controlled testing without real devices.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from AutoGLM_GUI.device_protocol import (
|
|
10
|
+
DeviceInfo,
|
|
11
|
+
DeviceManagerProtocol,
|
|
12
|
+
DeviceProtocol,
|
|
13
|
+
Screenshot,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from tests.integration.state_machine import StateMachine
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MockDevice(DeviceProtocol):
|
|
21
|
+
"""
|
|
22
|
+
Mock device implementation driven by a state machine.
|
|
23
|
+
|
|
24
|
+
All operations are routed through the state machine, which controls
|
|
25
|
+
screenshots and validates actions (tap coordinates, etc.).
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> from tests.integration.state_machine import load_test_case
|
|
29
|
+
>>> state_machine, instruction, max_steps = load_test_case("scenario.yaml")
|
|
30
|
+
>>>
|
|
31
|
+
>>> device = MockDevice("mock_001", state_machine)
|
|
32
|
+
>>> screenshot = device.get_screenshot() # Returns current state's screenshot
|
|
33
|
+
>>> device.tap(100, 200) # State machine validates and transitions
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, device_id: str, state_machine: "StateMachine"):
|
|
37
|
+
"""
|
|
38
|
+
Initialize mock device.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
device_id: Mock device ID.
|
|
42
|
+
state_machine: State machine that controls test flow.
|
|
43
|
+
"""
|
|
44
|
+
self._device_id = device_id
|
|
45
|
+
self._state_machine = state_machine
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def device_id(self) -> str:
|
|
49
|
+
"""Unique device identifier."""
|
|
50
|
+
return self._device_id
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def state_machine(self) -> "StateMachine":
|
|
54
|
+
"""Get the underlying state machine."""
|
|
55
|
+
return self._state_machine
|
|
56
|
+
|
|
57
|
+
# === Screenshot ===
|
|
58
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
59
|
+
"""Get screenshot from current state."""
|
|
60
|
+
result = self._state_machine.get_current_screenshot()
|
|
61
|
+
return Screenshot(
|
|
62
|
+
base64_data=result.base64_data,
|
|
63
|
+
width=result.width,
|
|
64
|
+
height=result.height,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# === Input Operations ===
|
|
68
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
69
|
+
"""Handle tap action through state machine."""
|
|
70
|
+
self._state_machine.handle_tap(x, y)
|
|
71
|
+
|
|
72
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
73
|
+
"""Handle double tap (treated as single tap)."""
|
|
74
|
+
self._state_machine.handle_tap(x, y)
|
|
75
|
+
|
|
76
|
+
def long_press(
|
|
77
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Handle long press (treated as tap for testing)."""
|
|
80
|
+
self._state_machine.handle_tap(x, y)
|
|
81
|
+
|
|
82
|
+
def swipe(
|
|
83
|
+
self,
|
|
84
|
+
start_x: int,
|
|
85
|
+
start_y: int,
|
|
86
|
+
end_x: int,
|
|
87
|
+
end_y: int,
|
|
88
|
+
duration_ms: int | None = None,
|
|
89
|
+
delay: float | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Handle swipe action."""
|
|
92
|
+
self._state_machine.handle_swipe(start_x, start_y, end_x, end_y)
|
|
93
|
+
|
|
94
|
+
def type_text(self, text: str) -> None:
|
|
95
|
+
"""Handle text input (no-op in testing)."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def clear_text(self) -> None:
|
|
99
|
+
"""Handle text clear (no-op in testing)."""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# === Navigation ===
|
|
103
|
+
def back(self, delay: float | None = None) -> None:
|
|
104
|
+
"""Handle back button (no-op in testing)."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def home(self, delay: float | None = None) -> None:
|
|
108
|
+
"""Handle home button (no-op in testing)."""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
112
|
+
"""Handle app launch (always succeeds in testing)."""
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
# === State Query ===
|
|
116
|
+
def get_current_app(self) -> str:
|
|
117
|
+
"""Get current app name from the current state."""
|
|
118
|
+
return self._state_machine.current_state.current_app
|
|
119
|
+
|
|
120
|
+
# === Keyboard Management ===
|
|
121
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
122
|
+
"""Mock keyboard detection."""
|
|
123
|
+
return "com.mock.keyboard"
|
|
124
|
+
|
|
125
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
126
|
+
"""Mock keyboard restore."""
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MockDeviceManager(DeviceManagerProtocol):
|
|
131
|
+
"""
|
|
132
|
+
Mock device manager for testing.
|
|
133
|
+
|
|
134
|
+
Provides a single mock device backed by a state machine.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> state_machine, _, _ = load_test_case("scenario.yaml")
|
|
138
|
+
>>> manager = MockDeviceManager(state_machine)
|
|
139
|
+
>>> device = manager.get_device("mock_001")
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
state_machine: "StateMachine",
|
|
145
|
+
device_id: str = "mock_device_001",
|
|
146
|
+
):
|
|
147
|
+
"""
|
|
148
|
+
Initialize mock device manager.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
state_machine: State machine for the mock device.
|
|
152
|
+
device_id: ID for the mock device.
|
|
153
|
+
"""
|
|
154
|
+
self._device_id = device_id
|
|
155
|
+
self._device = MockDevice(device_id, state_machine)
|
|
156
|
+
|
|
157
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
158
|
+
"""List the mock device."""
|
|
159
|
+
return [
|
|
160
|
+
DeviceInfo(
|
|
161
|
+
device_id=self._device_id,
|
|
162
|
+
status="online",
|
|
163
|
+
model="MockPhone",
|
|
164
|
+
platform="android",
|
|
165
|
+
connection_type="mock",
|
|
166
|
+
)
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
def get_device(self, device_id: str) -> MockDevice:
|
|
170
|
+
"""Get the mock device."""
|
|
171
|
+
if device_id != self._device_id:
|
|
172
|
+
raise KeyError(f"Device '{device_id}' not found")
|
|
173
|
+
return self._device
|
|
174
|
+
|
|
175
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
176
|
+
"""Mock connect (always succeeds)."""
|
|
177
|
+
return True, f"Connected to {address}"
|
|
178
|
+
|
|
179
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
180
|
+
"""Mock disconnect (always succeeds)."""
|
|
181
|
+
return True, f"Disconnected from {device_id}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Verify MockDeviceManager implements DeviceManagerProtocol
|
|
185
|
+
# Same issue as above - can't verify at import time
|