autoglm-gui 1.3.1__py3-none-any.whl → 1.4.1__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 (61) hide show
  1. AutoGLM_GUI/__main__.py +0 -4
  2. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  3. AutoGLM_GUI/agents/__init__.py +20 -0
  4. AutoGLM_GUI/agents/factory.py +160 -0
  5. AutoGLM_GUI/agents/mai_adapter.py +627 -0
  6. AutoGLM_GUI/agents/protocols.py +23 -0
  7. AutoGLM_GUI/api/__init__.py +50 -7
  8. AutoGLM_GUI/api/agents.py +61 -19
  9. AutoGLM_GUI/api/devices.py +12 -18
  10. AutoGLM_GUI/api/dual_model.py +24 -17
  11. AutoGLM_GUI/api/health.py +13 -0
  12. AutoGLM_GUI/api/layered_agent.py +659 -0
  13. AutoGLM_GUI/api/mcp.py +11 -10
  14. AutoGLM_GUI/api/version.py +23 -10
  15. AutoGLM_GUI/api/workflows.py +2 -1
  16. AutoGLM_GUI/config_manager.py +56 -24
  17. AutoGLM_GUI/device_adapter.py +263 -0
  18. AutoGLM_GUI/device_protocol.py +266 -0
  19. AutoGLM_GUI/devices/__init__.py +49 -0
  20. AutoGLM_GUI/devices/adb_device.py +205 -0
  21. AutoGLM_GUI/devices/mock_device.py +183 -0
  22. AutoGLM_GUI/devices/remote_device.py +172 -0
  23. AutoGLM_GUI/dual_model/decision_model.py +4 -4
  24. AutoGLM_GUI/dual_model/protocols.py +3 -3
  25. AutoGLM_GUI/exceptions.py +3 -3
  26. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +291 -0
  27. AutoGLM_GUI/metrics.py +13 -20
  28. AutoGLM_GUI/phone_agent_manager.py +219 -134
  29. AutoGLM_GUI/phone_agent_patches.py +2 -1
  30. AutoGLM_GUI/platform_utils.py +5 -2
  31. AutoGLM_GUI/prompts.py +6 -1
  32. AutoGLM_GUI/schemas.py +45 -14
  33. AutoGLM_GUI/scrcpy_stream.py +17 -13
  34. AutoGLM_GUI/server.py +3 -1
  35. AutoGLM_GUI/socketio_server.py +16 -4
  36. AutoGLM_GUI/state.py +10 -30
  37. AutoGLM_GUI/static/assets/{about-Cj6QXqMf.js → about-_XNhzQZX.js} +1 -1
  38. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +126 -0
  39. AutoGLM_GUI/static/assets/{dialog-CxJlnjzH.js → dialog-B3uW4T8V.js} +3 -3
  40. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +1 -0
  41. AutoGLM_GUI/static/assets/{index-C_B-Arvf.js → index-Cy8TmmHV.js} +1 -1
  42. AutoGLM_GUI/static/assets/{index-CxJQuE4y.js → index-UYYauTly.js} +6 -6
  43. AutoGLM_GUI/static/assets/{workflows-BTiGCNI0.js → workflows-Du_de-dt.js} +1 -1
  44. AutoGLM_GUI/static/index.html +2 -2
  45. AutoGLM_GUI/types.py +125 -0
  46. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/METADATA +147 -65
  47. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/RECORD +58 -39
  48. mai_agent/base.py +137 -0
  49. mai_agent/mai_grounding_agent.py +263 -0
  50. mai_agent/mai_naivigation_agent.py +526 -0
  51. mai_agent/prompt.py +148 -0
  52. mai_agent/unified_memory.py +67 -0
  53. mai_agent/utils.py +73 -0
  54. phone_agent/config/prompts.py +6 -1
  55. phone_agent/config/prompts_zh.py +6 -1
  56. AutoGLM_GUI/config.py +0 -23
  57. AutoGLM_GUI/static/assets/chat-BJeomZgh.js +0 -124
  58. AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +0 -1
  59. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/WHEEL +0 -0
  60. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/entry_points.txt +0 -0
  61. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.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" | "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,205 @@
1
+ """ADB Device implementation of DeviceProtocol.
2
+
3
+ This module wraps the existing phone_agent.adb module to provide
4
+ a DeviceProtocol-compliant implementation.
5
+ """
6
+
7
+ from phone_agent import adb
8
+ from phone_agent.adb import ADBConnection
9
+
10
+ from AutoGLM_GUI.device_protocol import (
11
+ DeviceInfo,
12
+ DeviceManagerProtocol,
13
+ DeviceProtocol,
14
+ Screenshot,
15
+ )
16
+
17
+
18
+ class ADBDevice:
19
+ """
20
+ ADB device implementation using local subprocess calls.
21
+
22
+ Wraps the existing phone_agent.adb module to provide a clean
23
+ DeviceProtocol interface.
24
+
25
+ Example:
26
+ >>> device = ADBDevice("emulator-5554")
27
+ >>> screenshot = device.get_screenshot()
28
+ >>> device.tap(100, 200)
29
+ >>> device.swipe(100, 200, 300, 400)
30
+ """
31
+
32
+ def __init__(self, device_id: str):
33
+ """
34
+ Initialize ADB device.
35
+
36
+ Args:
37
+ device_id: ADB device ID (e.g., "emulator-5554", "192.168.1.100:5555").
38
+ """
39
+ self._device_id = device_id
40
+
41
+ @property
42
+ def device_id(self) -> str:
43
+ """Unique device identifier."""
44
+ return self._device_id
45
+
46
+ # === Screenshot ===
47
+ def get_screenshot(self, timeout: int = 10) -> Screenshot:
48
+ """Capture current screen."""
49
+ result = adb.get_screenshot(self._device_id, timeout)
50
+ return Screenshot(
51
+ base64_data=result.base64_data,
52
+ width=result.width,
53
+ height=result.height,
54
+ is_sensitive=result.is_sensitive,
55
+ )
56
+
57
+ # === Input Operations ===
58
+ def tap(self, x: int, y: int, delay: float | None = None) -> None:
59
+ """Tap at specified coordinates."""
60
+ adb.tap(x, y, self._device_id, delay)
61
+
62
+ def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
63
+ """Double tap at specified coordinates."""
64
+ adb.double_tap(x, y, self._device_id, delay)
65
+
66
+ def long_press(
67
+ self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
68
+ ) -> None:
69
+ """Long press at specified coordinates."""
70
+ adb.long_press(x, y, duration_ms, self._device_id, delay)
71
+
72
+ def swipe(
73
+ self,
74
+ start_x: int,
75
+ start_y: int,
76
+ end_x: int,
77
+ end_y: int,
78
+ duration_ms: int | None = None,
79
+ delay: float | None = None,
80
+ ) -> None:
81
+ """Swipe from start to end coordinates."""
82
+ adb.swipe(start_x, start_y, end_x, end_y, duration_ms, self._device_id, delay)
83
+
84
+ def type_text(self, text: str) -> None:
85
+ """Type text into the currently focused input field."""
86
+ adb.type_text(text, self._device_id)
87
+
88
+ def clear_text(self) -> None:
89
+ """Clear text in the currently focused input field."""
90
+ adb.clear_text(self._device_id)
91
+
92
+ # === Navigation ===
93
+ def back(self, delay: float | None = None) -> None:
94
+ """Press the back button."""
95
+ adb.back(self._device_id, delay)
96
+
97
+ def home(self, delay: float | None = None) -> None:
98
+ """Press the home button."""
99
+ adb.home(self._device_id, delay)
100
+
101
+ def launch_app(self, app_name: str, delay: float | None = None) -> bool:
102
+ """Launch an app by name."""
103
+ return adb.launch_app(app_name, self._device_id, delay)
104
+
105
+ # === State Query ===
106
+ def get_current_app(self) -> str:
107
+ """Get the currently focused app name."""
108
+ return adb.get_current_app(self._device_id)
109
+
110
+ # === Keyboard Management ===
111
+ def detect_and_set_adb_keyboard(self) -> str:
112
+ """Detect current keyboard and switch to ADB Keyboard if needed."""
113
+ return adb.detect_and_set_adb_keyboard(self._device_id)
114
+
115
+ def restore_keyboard(self, ime: str) -> None:
116
+ """Restore the original keyboard IME."""
117
+ adb.restore_keyboard(ime, self._device_id)
118
+
119
+
120
+ # Verify ADBDevice implements DeviceProtocol
121
+ assert isinstance(ADBDevice("test"), DeviceProtocol)
122
+
123
+
124
+ class ADBDeviceManager:
125
+ """
126
+ ADB device manager implementation.
127
+
128
+ Manages multiple ADB devices and provides DeviceProtocol instances.
129
+
130
+ Example:
131
+ >>> manager = ADBDeviceManager()
132
+ >>> devices = manager.list_devices()
133
+ >>> device = manager.get_device("emulator-5554")
134
+ >>> device.tap(100, 200)
135
+ """
136
+
137
+ def __init__(self, adb_path: str = "adb"):
138
+ """
139
+ Initialize the device manager.
140
+
141
+ Args:
142
+ adb_path: Path to ADB executable.
143
+ """
144
+ self._connection = ADBConnection(adb_path)
145
+ self._devices: dict[str, ADBDevice] = {}
146
+
147
+ def list_devices(self) -> list[DeviceInfo]:
148
+ """List all available devices."""
149
+ adb_devices = self._connection.list_devices()
150
+ result = []
151
+
152
+ for dev in adb_devices:
153
+ result.append(
154
+ DeviceInfo(
155
+ device_id=dev.device_id,
156
+ status="online" if dev.status == "device" else dev.status,
157
+ model=dev.model,
158
+ platform="android",
159
+ connection_type=dev.connection_type.value,
160
+ )
161
+ )
162
+
163
+ return result
164
+
165
+ def get_device(self, device_id: str) -> ADBDevice:
166
+ """
167
+ Get a device instance by ID.
168
+
169
+ Args:
170
+ device_id: The device ID.
171
+
172
+ Returns:
173
+ ADBDevice instance.
174
+
175
+ Raises:
176
+ KeyError: If device not found or offline.
177
+ """
178
+ # Check if device exists and is online
179
+ devices = self.list_devices()
180
+ device_info = next((d for d in devices if d.device_id == device_id), None)
181
+
182
+ if device_info is None:
183
+ raise KeyError(f"Device '{device_id}' not found")
184
+ if device_info.status != "online":
185
+ raise KeyError(f"Device '{device_id}' is {device_info.status}")
186
+
187
+ # Cache and return device instance
188
+ if device_id not in self._devices:
189
+ self._devices[device_id] = ADBDevice(device_id)
190
+
191
+ return self._devices[device_id]
192
+
193
+ def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
194
+ """Connect to a remote device."""
195
+ return self._connection.connect(address, timeout)
196
+
197
+ def disconnect(self, device_id: str) -> tuple[bool, str]:
198
+ """Disconnect from a device."""
199
+ # Remove from cache
200
+ self._devices.pop(device_id, None)
201
+ return self._connection.disconnect(device_id)
202
+
203
+
204
+ # Verify ADBDeviceManager implements DeviceManagerProtocol
205
+ assert isinstance(ADBDeviceManager(), DeviceManagerProtocol)
@@ -0,0 +1,183 @@
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
+ Screenshot,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from tests.integration.state_machine import StateMachine
16
+
17
+
18
+ class MockDevice:
19
+ """
20
+ Mock device implementation driven by a state machine.
21
+
22
+ All operations are routed through the state machine, which controls
23
+ screenshots and validates actions (tap coordinates, etc.).
24
+
25
+ Example:
26
+ >>> from tests.integration.state_machine import load_test_case
27
+ >>> state_machine, instruction, max_steps = load_test_case("scenario.yaml")
28
+ >>>
29
+ >>> device = MockDevice("mock_001", state_machine)
30
+ >>> screenshot = device.get_screenshot() # Returns current state's screenshot
31
+ >>> device.tap(100, 200) # State machine validates and transitions
32
+ """
33
+
34
+ def __init__(self, device_id: str, state_machine: "StateMachine"):
35
+ """
36
+ Initialize mock device.
37
+
38
+ Args:
39
+ device_id: Mock device ID.
40
+ state_machine: State machine that controls test flow.
41
+ """
42
+ self._device_id = device_id
43
+ self._state_machine = state_machine
44
+
45
+ @property
46
+ def device_id(self) -> str:
47
+ """Unique device identifier."""
48
+ return self._device_id
49
+
50
+ @property
51
+ def state_machine(self) -> "StateMachine":
52
+ """Get the underlying state machine."""
53
+ return self._state_machine
54
+
55
+ # === Screenshot ===
56
+ def get_screenshot(self, timeout: int = 10) -> Screenshot:
57
+ """Get screenshot from current state."""
58
+ result = self._state_machine.get_current_screenshot()
59
+ return Screenshot(
60
+ base64_data=result.base64_data,
61
+ width=result.width,
62
+ height=result.height,
63
+ )
64
+
65
+ # === Input Operations ===
66
+ def tap(self, x: int, y: int, delay: float | None = None) -> None:
67
+ """Handle tap action through state machine."""
68
+ self._state_machine.handle_tap(x, y)
69
+
70
+ def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
71
+ """Handle double tap (treated as single tap)."""
72
+ self._state_machine.handle_tap(x, y)
73
+
74
+ def long_press(
75
+ self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
76
+ ) -> None:
77
+ """Handle long press (treated as tap for testing)."""
78
+ self._state_machine.handle_tap(x, y)
79
+
80
+ def swipe(
81
+ self,
82
+ start_x: int,
83
+ start_y: int,
84
+ end_x: int,
85
+ end_y: int,
86
+ duration_ms: int | None = None,
87
+ delay: float | None = None,
88
+ ) -> None:
89
+ """Handle swipe action."""
90
+ self._state_machine.handle_swipe(start_x, start_y, end_x, end_y)
91
+
92
+ def type_text(self, text: str) -> None:
93
+ """Handle text input (no-op in testing)."""
94
+ pass
95
+
96
+ def clear_text(self) -> None:
97
+ """Handle text clear (no-op in testing)."""
98
+ pass
99
+
100
+ # === Navigation ===
101
+ def back(self, delay: float | None = None) -> None:
102
+ """Handle back button (no-op in testing)."""
103
+ pass
104
+
105
+ def home(self, delay: float | None = None) -> None:
106
+ """Handle home button (no-op in testing)."""
107
+ pass
108
+
109
+ def launch_app(self, app_name: str, delay: float | None = None) -> bool:
110
+ """Handle app launch (always succeeds in testing)."""
111
+ return True
112
+
113
+ # === State Query ===
114
+ def get_current_app(self) -> str:
115
+ """Get current app name from the current state."""
116
+ return self._state_machine.current_state.current_app
117
+
118
+ # === Keyboard Management ===
119
+ def detect_and_set_adb_keyboard(self) -> str:
120
+ """Mock keyboard detection."""
121
+ return "com.mock.keyboard"
122
+
123
+ def restore_keyboard(self, ime: str) -> None:
124
+ """Mock keyboard restore."""
125
+ pass
126
+
127
+
128
+ class MockDeviceManager:
129
+ """
130
+ Mock device manager for testing.
131
+
132
+ Provides a single mock device backed by a state machine.
133
+
134
+ Example:
135
+ >>> state_machine, _, _ = load_test_case("scenario.yaml")
136
+ >>> manager = MockDeviceManager(state_machine)
137
+ >>> device = manager.get_device("mock_001")
138
+ """
139
+
140
+ def __init__(
141
+ self,
142
+ state_machine: "StateMachine",
143
+ device_id: str = "mock_device_001",
144
+ ):
145
+ """
146
+ Initialize mock device manager.
147
+
148
+ Args:
149
+ state_machine: State machine for the mock device.
150
+ device_id: ID for the mock device.
151
+ """
152
+ self._device_id = device_id
153
+ self._device = MockDevice(device_id, state_machine)
154
+
155
+ def list_devices(self) -> list[DeviceInfo]:
156
+ """List the mock device."""
157
+ return [
158
+ DeviceInfo(
159
+ device_id=self._device_id,
160
+ status="online",
161
+ model="MockPhone",
162
+ platform="android",
163
+ connection_type="mock",
164
+ )
165
+ ]
166
+
167
+ def get_device(self, device_id: str) -> MockDevice:
168
+ """Get the mock device."""
169
+ if device_id != self._device_id:
170
+ raise KeyError(f"Device '{device_id}' not found")
171
+ return self._device
172
+
173
+ def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
174
+ """Mock connect (always succeeds)."""
175
+ return True, f"Connected to {address}"
176
+
177
+ def disconnect(self, device_id: str) -> tuple[bool, str]:
178
+ """Mock disconnect (always succeeds)."""
179
+ return True, f"Disconnected from {device_id}"
180
+
181
+
182
+ # Verify MockDeviceManager implements DeviceManagerProtocol
183
+ # Same issue as above - can't verify at import time