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.
Files changed (120) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -8
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  15. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  16. AutoGLM_GUI/adb_plus/serial.py +38 -20
  17. AutoGLM_GUI/adb_plus/touch.py +4 -9
  18. AutoGLM_GUI/agents/__init__.py +51 -0
  19. AutoGLM_GUI/agents/events.py +19 -0
  20. AutoGLM_GUI/agents/factory.py +153 -0
  21. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  22. AutoGLM_GUI/agents/glm/agent.py +292 -0
  23. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  24. AutoGLM_GUI/agents/glm/parser.py +110 -0
  25. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  26. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  27. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  28. AutoGLM_GUI/agents/mai/agent.py +405 -0
  29. AutoGLM_GUI/agents/mai/parser.py +254 -0
  30. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  31. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  32. AutoGLM_GUI/agents/protocols.py +27 -0
  33. AutoGLM_GUI/agents/stream_runner.py +188 -0
  34. AutoGLM_GUI/api/__init__.py +71 -11
  35. AutoGLM_GUI/api/agents.py +190 -229
  36. AutoGLM_GUI/api/control.py +9 -6
  37. AutoGLM_GUI/api/devices.py +112 -28
  38. AutoGLM_GUI/api/health.py +13 -0
  39. AutoGLM_GUI/api/history.py +78 -0
  40. AutoGLM_GUI/api/layered_agent.py +306 -181
  41. AutoGLM_GUI/api/mcp.py +11 -10
  42. AutoGLM_GUI/api/media.py +64 -1
  43. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  44. AutoGLM_GUI/api/version.py +23 -10
  45. AutoGLM_GUI/api/workflows.py +2 -1
  46. AutoGLM_GUI/config.py +72 -14
  47. AutoGLM_GUI/config_manager.py +98 -27
  48. AutoGLM_GUI/device_adapter.py +263 -0
  49. AutoGLM_GUI/device_manager.py +248 -29
  50. AutoGLM_GUI/device_protocol.py +266 -0
  51. AutoGLM_GUI/devices/__init__.py +49 -0
  52. AutoGLM_GUI/devices/adb_device.py +200 -0
  53. AutoGLM_GUI/devices/mock_device.py +185 -0
  54. AutoGLM_GUI/devices/remote_device.py +177 -0
  55. AutoGLM_GUI/exceptions.py +3 -3
  56. AutoGLM_GUI/history_manager.py +164 -0
  57. AutoGLM_GUI/i18n.py +81 -0
  58. AutoGLM_GUI/metrics.py +13 -20
  59. AutoGLM_GUI/model/__init__.py +5 -0
  60. AutoGLM_GUI/model/message_builder.py +69 -0
  61. AutoGLM_GUI/model/types.py +24 -0
  62. AutoGLM_GUI/models/__init__.py +10 -0
  63. AutoGLM_GUI/models/history.py +96 -0
  64. AutoGLM_GUI/models/scheduled_task.py +71 -0
  65. AutoGLM_GUI/parsers/__init__.py +22 -0
  66. AutoGLM_GUI/parsers/base.py +50 -0
  67. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  68. AutoGLM_GUI/phone_agent_manager.py +118 -367
  69. AutoGLM_GUI/platform_utils.py +31 -2
  70. AutoGLM_GUI/prompt_config.py +15 -0
  71. AutoGLM_GUI/prompts/__init__.py +32 -0
  72. AutoGLM_GUI/scheduler_manager.py +304 -0
  73. AutoGLM_GUI/schemas.py +272 -63
  74. AutoGLM_GUI/scrcpy_stream.py +159 -37
  75. AutoGLM_GUI/server.py +3 -1
  76. AutoGLM_GUI/socketio_server.py +114 -29
  77. AutoGLM_GUI/state.py +10 -30
  78. AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
  79. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  80. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  81. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  82. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  83. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  84. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  85. AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
  86. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  87. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  88. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  89. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  90. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  91. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  92. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  93. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  94. AutoGLM_GUI/static/index.html +2 -2
  95. AutoGLM_GUI/types.py +142 -0
  96. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
  97. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  98. mai_agent/base.py +137 -0
  99. mai_agent/mai_grounding_agent.py +263 -0
  100. mai_agent/mai_naivigation_agent.py +526 -0
  101. mai_agent/prompt.py +148 -0
  102. mai_agent/unified_memory.py +67 -0
  103. mai_agent/utils.py +73 -0
  104. AutoGLM_GUI/api/dual_model.py +0 -311
  105. AutoGLM_GUI/dual_model/__init__.py +0 -53
  106. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  107. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  108. AutoGLM_GUI/dual_model/protocols.py +0 -354
  109. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  110. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  111. AutoGLM_GUI/phone_agent_patches.py +0 -146
  112. AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
  113. AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
  114. AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
  115. AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
  116. AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
  117. autoglm_gui-1.4.0.dist-info/RECORD +0 -100
  118. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  119. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  120. {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