autoglm-gui 0.2.0__py3-none-any.whl → 0.2.3__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.
@@ -0,0 +1,350 @@
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
+ from typing import Optional
8
+
9
+
10
+ class ConnectionType(Enum):
11
+ """Type of ADB connection."""
12
+
13
+ USB = "usb"
14
+ WIFI = "wifi"
15
+ REMOTE = "remote"
16
+
17
+
18
+ @dataclass
19
+ class DeviceInfo:
20
+ """Information about a connected device."""
21
+
22
+ device_id: str
23
+ status: str
24
+ connection_type: ConnectionType
25
+ model: str | None = None
26
+ android_version: str | None = None
27
+
28
+
29
+ class ADBConnection:
30
+ """
31
+ Manages ADB connections to Android devices.
32
+
33
+ Supports USB, WiFi, and remote TCP/IP connections.
34
+
35
+ Example:
36
+ >>> conn = ADBConnection()
37
+ >>> # Connect to remote device
38
+ >>> conn.connect("192.168.1.100:5555")
39
+ >>> # List devices
40
+ >>> devices = conn.list_devices()
41
+ >>> # Disconnect
42
+ >>> conn.disconnect("192.168.1.100:5555")
43
+ """
44
+
45
+ def __init__(self, adb_path: str = "adb"):
46
+ """
47
+ Initialize ADB connection manager.
48
+
49
+ Args:
50
+ adb_path: Path to ADB executable.
51
+ """
52
+ self.adb_path = adb_path
53
+
54
+ def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
55
+ """
56
+ Connect to a remote device via TCP/IP.
57
+
58
+ Args:
59
+ address: Device address in format "host:port" (e.g., "192.168.1.100:5555").
60
+ timeout: Connection timeout in seconds.
61
+
62
+ Returns:
63
+ Tuple of (success, message).
64
+
65
+ Note:
66
+ The remote device must have TCP/IP debugging enabled.
67
+ On the device, run: adb tcpip 5555
68
+ """
69
+ # Validate address format
70
+ if ":" not in address:
71
+ address = f"{address}:5555" # Default ADB port
72
+
73
+ try:
74
+ result = subprocess.run(
75
+ [self.adb_path, "connect", address],
76
+ capture_output=True,
77
+ text=True,
78
+ timeout=timeout,
79
+ )
80
+
81
+ output = result.stdout + result.stderr
82
+
83
+ if "connected" in output.lower():
84
+ return True, f"Connected to {address}"
85
+ elif "already connected" in output.lower():
86
+ return True, f"Already connected to {address}"
87
+ else:
88
+ return False, output.strip()
89
+
90
+ except subprocess.TimeoutExpired:
91
+ return False, f"Connection timeout after {timeout}s"
92
+ except Exception as e:
93
+ return False, f"Connection error: {e}"
94
+
95
+ def disconnect(self, address: str | None = None) -> tuple[bool, str]:
96
+ """
97
+ Disconnect from a remote device.
98
+
99
+ Args:
100
+ address: Device address to disconnect. If None, disconnects all.
101
+
102
+ Returns:
103
+ Tuple of (success, message).
104
+ """
105
+ try:
106
+ cmd = [self.adb_path, "disconnect"]
107
+ if address:
108
+ cmd.append(address)
109
+
110
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
111
+
112
+ output = result.stdout + result.stderr
113
+ return True, output.strip() or "Disconnected"
114
+
115
+ except Exception as e:
116
+ return False, f"Disconnect error: {e}"
117
+
118
+ def list_devices(self) -> list[DeviceInfo]:
119
+ """
120
+ List all connected devices.
121
+
122
+ Returns:
123
+ List of DeviceInfo objects.
124
+ """
125
+ try:
126
+ result = subprocess.run(
127
+ [self.adb_path, "devices", "-l"],
128
+ capture_output=True,
129
+ text=True,
130
+ timeout=5,
131
+ )
132
+
133
+ devices = []
134
+ for line in result.stdout.strip().split("\n")[1:]: # Skip header
135
+ if not line.strip():
136
+ continue
137
+
138
+ parts = line.split()
139
+ if len(parts) >= 2:
140
+ device_id = parts[0]
141
+ status = parts[1]
142
+
143
+ # Determine connection type
144
+ if ":" in device_id:
145
+ conn_type = ConnectionType.REMOTE
146
+ elif "emulator" in device_id:
147
+ conn_type = ConnectionType.USB # Emulator via USB
148
+ else:
149
+ conn_type = ConnectionType.USB
150
+
151
+ # Parse additional info
152
+ model = None
153
+ for part in parts[2:]:
154
+ if part.startswith("model:"):
155
+ model = part.split(":", 1)[1]
156
+ break
157
+
158
+ devices.append(
159
+ DeviceInfo(
160
+ device_id=device_id,
161
+ status=status,
162
+ connection_type=conn_type,
163
+ model=model,
164
+ )
165
+ )
166
+
167
+ return devices
168
+
169
+ except Exception as e:
170
+ print(f"Error listing devices: {e}")
171
+ return []
172
+
173
+ def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
174
+ """
175
+ Get detailed information about a device.
176
+
177
+ Args:
178
+ device_id: Device ID. If None, uses first available device.
179
+
180
+ Returns:
181
+ DeviceInfo or None if not found.
182
+ """
183
+ devices = self.list_devices()
184
+
185
+ if not devices:
186
+ return None
187
+
188
+ if device_id is None:
189
+ return devices[0]
190
+
191
+ for device in devices:
192
+ if device.device_id == device_id:
193
+ return device
194
+
195
+ return None
196
+
197
+ def is_connected(self, device_id: str | None = None) -> bool:
198
+ """
199
+ Check if a device is connected.
200
+
201
+ Args:
202
+ device_id: Device ID to check. If None, checks if any device is connected.
203
+
204
+ Returns:
205
+ True if connected, False otherwise.
206
+ """
207
+ devices = self.list_devices()
208
+
209
+ if not devices:
210
+ return False
211
+
212
+ if device_id is None:
213
+ return any(d.status == "device" for d in devices)
214
+
215
+ return any(d.device_id == device_id and d.status == "device" for d in devices)
216
+
217
+ def enable_tcpip(
218
+ self, port: int = 5555, device_id: str | None = None
219
+ ) -> tuple[bool, str]:
220
+ """
221
+ Enable TCP/IP debugging on a USB-connected device.
222
+
223
+ This allows subsequent wireless connections to the device.
224
+
225
+ Args:
226
+ port: TCP port for ADB (default: 5555).
227
+ device_id: Device ID. If None, uses first available device.
228
+
229
+ Returns:
230
+ Tuple of (success, message).
231
+
232
+ Note:
233
+ The device must be connected via USB first.
234
+ After this, you can disconnect USB and connect via WiFi.
235
+ """
236
+ try:
237
+ cmd = [self.adb_path]
238
+ if device_id:
239
+ cmd.extend(["-s", device_id])
240
+ cmd.extend(["tcpip", str(port)])
241
+
242
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
243
+
244
+ output = result.stdout + result.stderr
245
+
246
+ if "restarting" in output.lower() or result.returncode == 0:
247
+ time.sleep(2) # Wait for ADB to restart
248
+ return True, f"TCP/IP mode enabled on port {port}"
249
+ else:
250
+ return False, output.strip()
251
+
252
+ except Exception as e:
253
+ return False, f"Error enabling TCP/IP: {e}"
254
+
255
+ def get_device_ip(self, device_id: str | None = None) -> str | None:
256
+ """
257
+ Get the IP address of a connected device.
258
+
259
+ Args:
260
+ device_id: Device ID. If None, uses first available device.
261
+
262
+ Returns:
263
+ IP address string or None if not found.
264
+ """
265
+ try:
266
+ cmd = [self.adb_path]
267
+ if device_id:
268
+ cmd.extend(["-s", device_id])
269
+ cmd.extend(["shell", "ip", "route"])
270
+
271
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
272
+
273
+ # Parse IP from route output
274
+ for line in result.stdout.split("\n"):
275
+ if "src" in line:
276
+ parts = line.split()
277
+ for i, part in enumerate(parts):
278
+ if part == "src" and i + 1 < len(parts):
279
+ return parts[i + 1]
280
+
281
+ # Alternative: try wlan0 interface
282
+ cmd[-1] = "ip addr show wlan0"
283
+ result = subprocess.run(
284
+ cmd[:-1] + ["shell", "ip", "addr", "show", "wlan0"],
285
+ capture_output=True,
286
+ text=True,
287
+ timeout=5,
288
+ )
289
+
290
+ for line in result.stdout.split("\n"):
291
+ if "inet " in line:
292
+ parts = line.strip().split()
293
+ if len(parts) >= 2:
294
+ return parts[1].split("/")[0]
295
+
296
+ return None
297
+
298
+ except Exception as e:
299
+ print(f"Error getting device IP: {e}")
300
+ return None
301
+
302
+ def restart_server(self) -> tuple[bool, str]:
303
+ """
304
+ Restart the ADB server.
305
+
306
+ Returns:
307
+ Tuple of (success, message).
308
+ """
309
+ try:
310
+ # Kill server
311
+ subprocess.run(
312
+ [self.adb_path, "kill-server"], capture_output=True, timeout=5
313
+ )
314
+
315
+ time.sleep(1)
316
+
317
+ # Start server
318
+ subprocess.run(
319
+ [self.adb_path, "start-server"], capture_output=True, timeout=5
320
+ )
321
+
322
+ return True, "ADB server restarted"
323
+
324
+ except Exception as e:
325
+ return False, f"Error restarting server: {e}"
326
+
327
+
328
+ def quick_connect(address: str) -> tuple[bool, str]:
329
+ """
330
+ Quick helper to connect to a remote device.
331
+
332
+ Args:
333
+ address: Device address (e.g., "192.168.1.100" or "192.168.1.100:5555").
334
+
335
+ Returns:
336
+ Tuple of (success, message).
337
+ """
338
+ conn = ADBConnection()
339
+ return conn.connect(address)
340
+
341
+
342
+ def list_devices() -> list[DeviceInfo]:
343
+ """
344
+ Quick helper to list connected devices.
345
+
346
+ Returns:
347
+ List of DeviceInfo objects.
348
+ """
349
+ conn = ADBConnection()
350
+ return conn.list_devices()
@@ -0,0 +1,224 @@
1
+ """Device control utilities for Android automation."""
2
+
3
+ import os
4
+ import subprocess
5
+ import time
6
+ from typing import List, Optional, Tuple
7
+
8
+ from phone_agent.config.apps import APP_PACKAGES
9
+
10
+
11
+ def get_current_app(device_id: str | None = None) -> str:
12
+ """
13
+ Get the currently focused app name.
14
+
15
+ Args:
16
+ device_id: Optional ADB device ID for multi-device setups.
17
+
18
+ Returns:
19
+ The app name if recognized, otherwise "System Home".
20
+ """
21
+ adb_prefix = _get_adb_prefix(device_id)
22
+
23
+ result = subprocess.run(
24
+ adb_prefix + ["shell", "dumpsys", "window"], capture_output=True, text=True
25
+ )
26
+ output = result.stdout
27
+
28
+ # Parse window focus info
29
+ for line in output.split("\n"):
30
+ if "mCurrentFocus" in line or "mFocusedApp" in line:
31
+ for app_name, package in APP_PACKAGES.items():
32
+ if package in line:
33
+ return app_name
34
+
35
+ return "System Home"
36
+
37
+
38
+ def tap(x: int, y: int, device_id: str | None = None, delay: float = 1.0) -> None:
39
+ """
40
+ Tap at the specified coordinates.
41
+
42
+ Args:
43
+ x: X coordinate.
44
+ y: Y coordinate.
45
+ device_id: Optional ADB device ID.
46
+ delay: Delay in seconds after tap.
47
+ """
48
+ adb_prefix = _get_adb_prefix(device_id)
49
+
50
+ subprocess.run(
51
+ adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
52
+ )
53
+ time.sleep(delay)
54
+
55
+
56
+ def double_tap(
57
+ x: int, y: int, device_id: str | None = None, delay: float = 1.0
58
+ ) -> None:
59
+ """
60
+ Double tap at the specified coordinates.
61
+
62
+ Args:
63
+ x: X coordinate.
64
+ y: Y coordinate.
65
+ device_id: Optional ADB device ID.
66
+ delay: Delay in seconds after double tap.
67
+ """
68
+ adb_prefix = _get_adb_prefix(device_id)
69
+
70
+ subprocess.run(
71
+ adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
72
+ )
73
+ time.sleep(0.1)
74
+ subprocess.run(
75
+ adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
76
+ )
77
+ time.sleep(delay)
78
+
79
+
80
+ def long_press(
81
+ x: int,
82
+ y: int,
83
+ duration_ms: int = 3000,
84
+ device_id: str | None = None,
85
+ delay: float = 1.0,
86
+ ) -> None:
87
+ """
88
+ Long press at the specified coordinates.
89
+
90
+ Args:
91
+ x: X coordinate.
92
+ y: Y coordinate.
93
+ duration_ms: Duration of press in milliseconds.
94
+ device_id: Optional ADB device ID.
95
+ delay: Delay in seconds after long press.
96
+ """
97
+ adb_prefix = _get_adb_prefix(device_id)
98
+
99
+ subprocess.run(
100
+ adb_prefix
101
+ + ["shell", "input", "swipe", str(x), str(y), str(x), str(y), str(duration_ms)],
102
+ capture_output=True,
103
+ )
104
+ time.sleep(delay)
105
+
106
+
107
+ def swipe(
108
+ start_x: int,
109
+ start_y: int,
110
+ end_x: int,
111
+ end_y: int,
112
+ duration_ms: int | None = None,
113
+ device_id: str | None = None,
114
+ delay: float = 1.0,
115
+ ) -> None:
116
+ """
117
+ Swipe from start to end coordinates.
118
+
119
+ Args:
120
+ start_x: Starting X coordinate.
121
+ start_y: Starting Y coordinate.
122
+ end_x: Ending X coordinate.
123
+ end_y: Ending Y coordinate.
124
+ duration_ms: Duration of swipe in milliseconds (auto-calculated if None).
125
+ device_id: Optional ADB device ID.
126
+ delay: Delay in seconds after swipe.
127
+ """
128
+ adb_prefix = _get_adb_prefix(device_id)
129
+
130
+ if duration_ms is None:
131
+ # Calculate duration based on distance
132
+ dist_sq = (start_x - end_x) ** 2 + (start_y - end_y) ** 2
133
+ duration_ms = int(dist_sq / 1000)
134
+ duration_ms = max(1000, min(duration_ms, 2000)) # Clamp between 1000-2000ms
135
+
136
+ subprocess.run(
137
+ adb_prefix
138
+ + [
139
+ "shell",
140
+ "input",
141
+ "swipe",
142
+ str(start_x),
143
+ str(start_y),
144
+ str(end_x),
145
+ str(end_y),
146
+ str(duration_ms),
147
+ ],
148
+ capture_output=True,
149
+ )
150
+ time.sleep(delay)
151
+
152
+
153
+ def back(device_id: str | None = None, delay: float = 1.0) -> None:
154
+ """
155
+ Press the back button.
156
+
157
+ Args:
158
+ device_id: Optional ADB device ID.
159
+ delay: Delay in seconds after pressing back.
160
+ """
161
+ adb_prefix = _get_adb_prefix(device_id)
162
+
163
+ subprocess.run(
164
+ adb_prefix + ["shell", "input", "keyevent", "4"], capture_output=True
165
+ )
166
+ time.sleep(delay)
167
+
168
+
169
+ def home(device_id: str | None = None, delay: float = 1.0) -> None:
170
+ """
171
+ Press the home button.
172
+
173
+ Args:
174
+ device_id: Optional ADB device ID.
175
+ delay: Delay in seconds after pressing home.
176
+ """
177
+ adb_prefix = _get_adb_prefix(device_id)
178
+
179
+ subprocess.run(
180
+ adb_prefix + ["shell", "input", "keyevent", "KEYCODE_HOME"], capture_output=True
181
+ )
182
+ time.sleep(delay)
183
+
184
+
185
+ def launch_app(app_name: str, device_id: str | None = None, delay: float = 1.0) -> bool:
186
+ """
187
+ Launch an app by name.
188
+
189
+ Args:
190
+ app_name: The app name (must be in APP_PACKAGES).
191
+ device_id: Optional ADB device ID.
192
+ delay: Delay in seconds after launching.
193
+
194
+ Returns:
195
+ True if app was launched, False if app not found.
196
+ """
197
+ if app_name not in APP_PACKAGES:
198
+ return False
199
+
200
+ adb_prefix = _get_adb_prefix(device_id)
201
+ package = APP_PACKAGES[app_name]
202
+
203
+ subprocess.run(
204
+ adb_prefix
205
+ + [
206
+ "shell",
207
+ "monkey",
208
+ "-p",
209
+ package,
210
+ "-c",
211
+ "android.intent.category.LAUNCHER",
212
+ "1",
213
+ ],
214
+ capture_output=True,
215
+ )
216
+ time.sleep(delay)
217
+ return True
218
+
219
+
220
+ def _get_adb_prefix(device_id: str | None) -> list:
221
+ """Get ADB command prefix with optional device specifier."""
222
+ if device_id:
223
+ return ["adb", "-s", device_id]
224
+ return ["adb"]
@@ -0,0 +1,109 @@
1
+ """Input utilities for Android device text input."""
2
+
3
+ import base64
4
+ import subprocess
5
+ from typing import Optional
6
+
7
+
8
+ def type_text(text: str, device_id: str | None = None) -> None:
9
+ """
10
+ Type text into the currently focused input field using ADB Keyboard.
11
+
12
+ Args:
13
+ text: The text to type.
14
+ device_id: Optional ADB device ID for multi-device setups.
15
+
16
+ Note:
17
+ Requires ADB Keyboard to be installed on the device.
18
+ See: https://github.com/nicnocquee/AdbKeyboard
19
+ """
20
+ adb_prefix = _get_adb_prefix(device_id)
21
+ encoded_text = base64.b64encode(text.encode("utf-8")).decode("utf-8")
22
+
23
+ subprocess.run(
24
+ adb_prefix
25
+ + [
26
+ "shell",
27
+ "am",
28
+ "broadcast",
29
+ "-a",
30
+ "ADB_INPUT_B64",
31
+ "--es",
32
+ "msg",
33
+ encoded_text,
34
+ ],
35
+ capture_output=True,
36
+ text=True,
37
+ )
38
+
39
+
40
+ def clear_text(device_id: str | None = None) -> None:
41
+ """
42
+ Clear text in the currently focused input field.
43
+
44
+ Args:
45
+ device_id: Optional ADB device ID for multi-device setups.
46
+ """
47
+ adb_prefix = _get_adb_prefix(device_id)
48
+
49
+ subprocess.run(
50
+ adb_prefix + ["shell", "am", "broadcast", "-a", "ADB_CLEAR_TEXT"],
51
+ capture_output=True,
52
+ text=True,
53
+ )
54
+
55
+
56
+ def detect_and_set_adb_keyboard(device_id: str | None = None) -> str:
57
+ """
58
+ Detect current keyboard and switch to ADB Keyboard if needed.
59
+
60
+ Args:
61
+ device_id: Optional ADB device ID for multi-device setups.
62
+
63
+ Returns:
64
+ The original keyboard IME identifier for later restoration.
65
+ """
66
+ adb_prefix = _get_adb_prefix(device_id)
67
+
68
+ # Get current IME
69
+ result = subprocess.run(
70
+ adb_prefix + ["shell", "settings", "get", "secure", "default_input_method"],
71
+ capture_output=True,
72
+ text=True,
73
+ )
74
+ current_ime = (result.stdout + result.stderr).strip()
75
+
76
+ # Switch to ADB Keyboard if not already set
77
+ if "com.android.adbkeyboard/.AdbIME" not in current_ime:
78
+ subprocess.run(
79
+ adb_prefix + ["shell", "ime", "set", "com.android.adbkeyboard/.AdbIME"],
80
+ capture_output=True,
81
+ text=True,
82
+ )
83
+
84
+ # Warm up the keyboard
85
+ type_text("", device_id)
86
+
87
+ return current_ime
88
+
89
+
90
+ def restore_keyboard(ime: str, device_id: str | None = None) -> None:
91
+ """
92
+ Restore the original keyboard IME.
93
+
94
+ Args:
95
+ ime: The IME identifier to restore.
96
+ device_id: Optional ADB device ID for multi-device setups.
97
+ """
98
+ adb_prefix = _get_adb_prefix(device_id)
99
+
100
+ subprocess.run(
101
+ adb_prefix + ["shell", "ime", "set", ime], capture_output=True, text=True
102
+ )
103
+
104
+
105
+ def _get_adb_prefix(device_id: str | None) -> list:
106
+ """Get ADB command prefix with optional device specifier."""
107
+ if device_id:
108
+ return ["adb", "-s", device_id]
109
+ return ["adb"]