voidremote 1.0.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.
- voidremote/__init__.py +31 -0
- voidremote/adb/__init__.py +28 -0
- voidremote/adb/client.py +385 -0
- voidremote/adb/device_parser.py +286 -0
- voidremote/cli/__init__.py +5 -0
- voidremote/cli/main.py +987 -0
- voidremote/config/__init__.py +37 -0
- voidremote/config/settings.py +203 -0
- voidremote/controllers/__init__.py +5 -0
- voidremote/controllers/app_controller.py +222 -0
- voidremote/core/__init__.py +1 -0
- voidremote/core/automation.py +184 -0
- voidremote/models/__init__.py +31 -0
- voidremote/models/device.py +263 -0
- voidremote/network/__init__.py +1 -0
- voidremote/network/discovery.py +116 -0
- voidremote/services/__init__.py +13 -0
- voidremote/services/device_service.py +340 -0
- voidremote/services/input_service.py +181 -0
- voidremote/services/monitor_service.py +198 -0
- voidremote/ui/__init__.py +1 -0
- voidremote/ui/app.py +70 -0
- voidremote/ui/dialogs/__init__.py +6 -0
- voidremote/ui/dialogs/connect_dialog.py +149 -0
- voidremote/ui/dialogs/pair_dialog.py +189 -0
- voidremote/ui/main_window.py +371 -0
- voidremote/ui/theme.py +529 -0
- voidremote/ui/views/__init__.py +15 -0
- voidremote/ui/views/dashboard_view.py +233 -0
- voidremote/ui/views/files_view.py +305 -0
- voidremote/ui/views/monitor_view.py +236 -0
- voidremote/ui/views/settings_view.py +257 -0
- voidremote/ui/views/shell_view.py +234 -0
- voidremote/ui/widgets/__init__.py +14 -0
- voidremote/ui/widgets/device_card.py +259 -0
- voidremote/ui/widgets/log_view.py +159 -0
- voidremote/ui/widgets/metric_gauge.py +90 -0
- voidremote/utils/__init__.py +28 -0
- voidremote/utils/logging.py +120 -0
- voidremote/utils/security.py +216 -0
- voidremote-1.0.0.dist-info/METADATA +578 -0
- voidremote-1.0.0.dist-info/RECORD +46 -0
- voidremote-1.0.0.dist-info/WHEEL +5 -0
- voidremote-1.0.0.dist-info/entry_points.txt +5 -0
- voidremote-1.0.0.dist-info/licenses/LICENSE +21 -0
- voidremote-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Device management service.
|
|
3
|
+
|
|
4
|
+
Provides high-level operations over the ADB client: device discovery,
|
|
5
|
+
pairing, connection, info retrieval, and trusted device persistence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from voidremote.adb.client import AdbClient, AdbError
|
|
17
|
+
from voidremote.adb.device_parser import (
|
|
18
|
+
build_device_from_raw,
|
|
19
|
+
parse_devices_output,
|
|
20
|
+
)
|
|
21
|
+
from voidremote.config.settings import TRUSTED_DEVICES_FILE
|
|
22
|
+
from voidremote.models.device import (
|
|
23
|
+
ConnectionConfig,
|
|
24
|
+
Device,
|
|
25
|
+
DeviceState,
|
|
26
|
+
PairingInfo,
|
|
27
|
+
TrustedDevice,
|
|
28
|
+
)
|
|
29
|
+
from voidremote.utils.security import validate_host, validate_port
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DeviceService:
|
|
35
|
+
"""
|
|
36
|
+
Orchestrates device discovery, connection, and persistent state.
|
|
37
|
+
|
|
38
|
+
This is the primary interface used by both the CLI and GUI to interact
|
|
39
|
+
with Android devices.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, adb: AdbClient) -> None:
|
|
43
|
+
self._adb = adb
|
|
44
|
+
self._devices: dict[str, Device] = {}
|
|
45
|
+
self._trusted: dict[str, TrustedDevice] = {}
|
|
46
|
+
self._load_trusted_devices()
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# Device discovery
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def refresh_devices(self) -> list[Device]:
|
|
53
|
+
"""
|
|
54
|
+
Refresh the list of connected ADB devices.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of current Device objects.
|
|
58
|
+
"""
|
|
59
|
+
raw = self._adb.list_devices_raw()
|
|
60
|
+
entries = parse_devices_output(raw)
|
|
61
|
+
discovered: dict[str, Device] = {}
|
|
62
|
+
|
|
63
|
+
for entry in entries:
|
|
64
|
+
serial = entry["serial"]
|
|
65
|
+
state = entry["state"]
|
|
66
|
+
try:
|
|
67
|
+
device = self._get_device_with_info(serial, state)
|
|
68
|
+
discovered[serial] = device
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
logger.warning("Failed to get info for %s: %s", serial, exc)
|
|
71
|
+
# Still add with minimal info
|
|
72
|
+
existing = self._devices.get(serial)
|
|
73
|
+
if existing:
|
|
74
|
+
discovered[serial] = existing
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
self._devices = discovered
|
|
78
|
+
logger.debug("Refreshed devices: %d found", len(self._devices))
|
|
79
|
+
return list(self._devices.values())
|
|
80
|
+
|
|
81
|
+
def get_device(self, serial: str) -> Optional[Device]:
|
|
82
|
+
"""Return device by serial, or None if not found."""
|
|
83
|
+
return self._devices.get(serial)
|
|
84
|
+
|
|
85
|
+
def list_devices(self) -> list[Device]:
|
|
86
|
+
"""Return currently cached device list."""
|
|
87
|
+
return list(self._devices.values())
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Pairing & connection
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def pair_device(self, host: str, port: int, code: str) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Pair a device over wireless debugging.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
host: Device IP address.
|
|
99
|
+
port: Pairing port (shown in wireless debug settings).
|
|
100
|
+
code: 6-digit pairing code.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if pairing succeeded.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
AdbError: If pairing fails.
|
|
107
|
+
ValueError: If parameters are invalid.
|
|
108
|
+
"""
|
|
109
|
+
validated_host = validate_host(host)
|
|
110
|
+
validated_port = validate_port(port)
|
|
111
|
+
pairing = PairingInfo(
|
|
112
|
+
host=validated_host,
|
|
113
|
+
port=validated_port,
|
|
114
|
+
pairing_code=code,
|
|
115
|
+
)
|
|
116
|
+
result = self._adb.pair(pairing)
|
|
117
|
+
success = "successfully paired" in result.stdout.lower()
|
|
118
|
+
if success:
|
|
119
|
+
logger.info("Paired successfully with %s:%d", validated_host, validated_port)
|
|
120
|
+
return success
|
|
121
|
+
|
|
122
|
+
def connect_device(
|
|
123
|
+
self,
|
|
124
|
+
host: str,
|
|
125
|
+
port: int = 5555,
|
|
126
|
+
remember: bool = True,
|
|
127
|
+
) -> Device:
|
|
128
|
+
"""
|
|
129
|
+
Connect to an Android device over TCP/IP.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
host: Device IP address.
|
|
133
|
+
port: ADB TCP port (default 5555).
|
|
134
|
+
remember: Whether to add device to trusted list.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The connected Device object.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
AdbError: If connection fails.
|
|
141
|
+
"""
|
|
142
|
+
validated_host = validate_host(host)
|
|
143
|
+
validated_port = validate_port(port)
|
|
144
|
+
config = ConnectionConfig(host=validated_host, port=validated_port)
|
|
145
|
+
self._adb.connect(config)
|
|
146
|
+
|
|
147
|
+
# Refresh to get real device info
|
|
148
|
+
self.refresh_devices()
|
|
149
|
+
serial = f"{validated_host}:{validated_port}"
|
|
150
|
+
device = self._devices.get(serial)
|
|
151
|
+
|
|
152
|
+
if device is None:
|
|
153
|
+
raise AdbError(f"Device {serial} not found after connect")
|
|
154
|
+
|
|
155
|
+
if remember:
|
|
156
|
+
self._save_trusted(device)
|
|
157
|
+
|
|
158
|
+
return device
|
|
159
|
+
|
|
160
|
+
def disconnect_device(self, serial: str) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Disconnect a wireless device.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
serial: Device serial (host:port format).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if disconnect succeeded.
|
|
169
|
+
"""
|
|
170
|
+
if ":" not in serial:
|
|
171
|
+
logger.warning("disconnect_device called on USB device %s — skipping", serial)
|
|
172
|
+
return False
|
|
173
|
+
host, port_str = serial.rsplit(":", 1)
|
|
174
|
+
try:
|
|
175
|
+
port = int(port_str)
|
|
176
|
+
except ValueError:
|
|
177
|
+
port = 5555
|
|
178
|
+
self._adb.disconnect(host, port)
|
|
179
|
+
if serial in self._devices:
|
|
180
|
+
self._devices[serial].state = DeviceState.DISCONNECTED
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
# Device info
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _get_device_with_info(self, serial: str, state: str) -> Device:
|
|
188
|
+
"""Build a Device with extended info from getprop & other sources."""
|
|
189
|
+
props: dict[str, str] = {}
|
|
190
|
+
extended: dict[str, str] = {}
|
|
191
|
+
|
|
192
|
+
if state == "device":
|
|
193
|
+
try:
|
|
194
|
+
props = self._fetch_getprop(serial)
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
logger.warning("getprop failed for %s: %s", serial, exc)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
extended["battery_raw"] = self._adb.shell(
|
|
200
|
+
serial, "dumpsys battery"
|
|
201
|
+
).stdout
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
extended["resolution"] = self._adb.shell(
|
|
207
|
+
serial, "wm size"
|
|
208
|
+
).stdout
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
extended["density"] = self._adb.shell(
|
|
214
|
+
serial, "wm density"
|
|
215
|
+
).stdout
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
extended["meminfo"] = self._adb.shell(
|
|
221
|
+
serial, "cat /proc/meminfo"
|
|
222
|
+
).stdout
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
extended["ip_address"] = self._parse_ip(serial)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
extended["storage_raw"] = self._adb.shell(
|
|
233
|
+
serial, "df /data"
|
|
234
|
+
).stdout
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
device = build_device_from_raw(serial, state, props, extended)
|
|
239
|
+
return device
|
|
240
|
+
|
|
241
|
+
def _fetch_getprop(self, serial: str) -> dict[str, str]:
|
|
242
|
+
"""Fetch all device properties via getprop."""
|
|
243
|
+
result = self._adb.shell(serial, "getprop")
|
|
244
|
+
props: dict[str, str] = {}
|
|
245
|
+
for line in result.stdout.splitlines():
|
|
246
|
+
m = __import__("re").match(r"\[([^\]]+)\]:\s*\[([^\]]*)\]", line)
|
|
247
|
+
if m:
|
|
248
|
+
props[m.group(1)] = m.group(2)
|
|
249
|
+
return props
|
|
250
|
+
|
|
251
|
+
def _parse_ip(self, serial: str) -> str:
|
|
252
|
+
"""Get the device's wlan0 IP address."""
|
|
253
|
+
from voidremote.adb.device_parser import parse_ip_address
|
|
254
|
+
result = self._adb.shell(serial, "ip addr show wlan0")
|
|
255
|
+
return parse_ip_address(result.stdout)
|
|
256
|
+
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
# Trusted devices
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def _save_trusted(self, device: Device) -> None:
|
|
262
|
+
"""Persist a device to the trusted devices store."""
|
|
263
|
+
serial = device.serial
|
|
264
|
+
trusted = TrustedDevice(
|
|
265
|
+
serial=serial,
|
|
266
|
+
host=device.host,
|
|
267
|
+
port=device.port,
|
|
268
|
+
alias=device.alias,
|
|
269
|
+
last_connected=datetime.now(),
|
|
270
|
+
)
|
|
271
|
+
self._trusted[serial] = trusted
|
|
272
|
+
self._persist_trusted()
|
|
273
|
+
logger.debug("Saved trusted device: %s", serial)
|
|
274
|
+
|
|
275
|
+
def _load_trusted_devices(self) -> None:
|
|
276
|
+
"""Load trusted devices from disk."""
|
|
277
|
+
if not TRUSTED_DEVICES_FILE.exists():
|
|
278
|
+
return
|
|
279
|
+
try:
|
|
280
|
+
data: list[dict] = json.loads(
|
|
281
|
+
TRUSTED_DEVICES_FILE.read_text(encoding="utf-8")
|
|
282
|
+
)
|
|
283
|
+
for entry in data:
|
|
284
|
+
td = TrustedDevice(**entry)
|
|
285
|
+
self._trusted[td.serial] = td
|
|
286
|
+
logger.debug("Loaded %d trusted devices", len(self._trusted))
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
logger.warning("Failed to load trusted devices: %s", exc)
|
|
289
|
+
|
|
290
|
+
def _persist_trusted(self) -> None:
|
|
291
|
+
"""Write trusted devices to disk."""
|
|
292
|
+
TRUSTED_DEVICES_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
293
|
+
entries = []
|
|
294
|
+
for td in self._trusted.values():
|
|
295
|
+
entry = {
|
|
296
|
+
"serial": td.serial,
|
|
297
|
+
"host": td.host,
|
|
298
|
+
"port": td.port,
|
|
299
|
+
"alias": td.alias,
|
|
300
|
+
"last_connected": (
|
|
301
|
+
td.last_connected.isoformat() if td.last_connected else None
|
|
302
|
+
),
|
|
303
|
+
"auto_connect": td.auto_connect,
|
|
304
|
+
"tags": td.tags,
|
|
305
|
+
}
|
|
306
|
+
entries.append(entry)
|
|
307
|
+
TRUSTED_DEVICES_FILE.write_text(
|
|
308
|
+
json.dumps(entries, indent=2), encoding="utf-8"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def list_trusted_devices(self) -> list[TrustedDevice]:
|
|
312
|
+
"""Return all trusted devices."""
|
|
313
|
+
return list(self._trusted.values())
|
|
314
|
+
|
|
315
|
+
def forget_device(self, serial: str) -> bool:
|
|
316
|
+
"""Remove a device from the trusted list."""
|
|
317
|
+
if serial in self._trusted:
|
|
318
|
+
del self._trusted[serial]
|
|
319
|
+
self._persist_trusted()
|
|
320
|
+
return True
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
def auto_reconnect_trusted(self) -> list[Device]:
|
|
324
|
+
"""
|
|
325
|
+
Attempt to reconnect all auto_connect trusted devices.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List of successfully reconnected devices.
|
|
329
|
+
"""
|
|
330
|
+
reconnected: list[Device] = []
|
|
331
|
+
for td in self._trusted.values():
|
|
332
|
+
if not td.auto_connect or not td.host:
|
|
333
|
+
continue
|
|
334
|
+
try:
|
|
335
|
+
device = self.connect_device(td.host, td.port, remember=False)
|
|
336
|
+
reconnected.append(device)
|
|
337
|
+
logger.info("Auto-reconnected: %s", td.serial)
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
logger.debug("Auto-reconnect failed for %s: %s", td.serial, exc)
|
|
340
|
+
return reconnected
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input control service for Android devices.
|
|
3
|
+
|
|
4
|
+
Provides mouse clicks, swipes, keyboard input, and hardware key events
|
|
5
|
+
via ADB input commands.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from enum import IntEnum
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from voidremote.adb.client import AdbClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KeyCode(IntEnum):
|
|
21
|
+
"""Android key event codes (subset)."""
|
|
22
|
+
|
|
23
|
+
KEYCODE_HOME = 3
|
|
24
|
+
KEYCODE_BACK = 4
|
|
25
|
+
KEYCODE_CALL = 5
|
|
26
|
+
KEYCODE_ENDCALL = 6
|
|
27
|
+
KEYCODE_DPAD_UP = 19
|
|
28
|
+
KEYCODE_DPAD_DOWN = 20
|
|
29
|
+
KEYCODE_DPAD_LEFT = 21
|
|
30
|
+
KEYCODE_DPAD_RIGHT = 22
|
|
31
|
+
KEYCODE_DPAD_CENTER = 23
|
|
32
|
+
KEYCODE_VOLUME_UP = 24
|
|
33
|
+
KEYCODE_VOLUME_DOWN = 25
|
|
34
|
+
KEYCODE_POWER = 26
|
|
35
|
+
KEYCODE_CAMERA = 27
|
|
36
|
+
KEYCODE_MENU = 82
|
|
37
|
+
KEYCODE_SEARCH = 84
|
|
38
|
+
KEYCODE_ENTER = 66
|
|
39
|
+
KEYCODE_DEL = 67
|
|
40
|
+
KEYCODE_ESCAPE = 111
|
|
41
|
+
KEYCODE_APP_SWITCH = 187
|
|
42
|
+
KEYCODE_SLEEP = 223
|
|
43
|
+
KEYCODE_WAKEUP = 224
|
|
44
|
+
KEYCODE_BRIGHTNESS_DOWN = 220
|
|
45
|
+
KEYCODE_BRIGHTNESS_UP = 221
|
|
46
|
+
KEYCODE_MEDIA_PLAY_PAUSE = 85
|
|
47
|
+
KEYCODE_MEDIA_NEXT = 87
|
|
48
|
+
KEYCODE_MEDIA_PREVIOUS = 88
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InputService:
|
|
52
|
+
"""
|
|
53
|
+
Sends touch/input events to an Android device via `adb shell input`.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, adb: AdbClient) -> None:
|
|
57
|
+
self._adb = adb
|
|
58
|
+
|
|
59
|
+
def tap(self, serial: str, x: int, y: int) -> None:
|
|
60
|
+
"""Single tap at screen coordinates."""
|
|
61
|
+
logger.debug("tap(%s) -> %d,%d", serial, x, y)
|
|
62
|
+
self._adb.shell(serial, f"input tap {x} {y}")
|
|
63
|
+
|
|
64
|
+
def double_tap(self, serial: str, x: int, y: int, delay_ms: int = 100) -> None:
|
|
65
|
+
"""Double tap at screen coordinates."""
|
|
66
|
+
self.tap(serial, x, y)
|
|
67
|
+
time.sleep(delay_ms / 1000.0)
|
|
68
|
+
self.tap(serial, x, y)
|
|
69
|
+
|
|
70
|
+
def long_press(
|
|
71
|
+
self, serial: str, x: int, y: int, duration_ms: int = 1000
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Long press at coordinates for specified duration."""
|
|
74
|
+
logger.debug("long_press(%s) -> %d,%d (%dms)", serial, x, y, duration_ms)
|
|
75
|
+
self._adb.shell(
|
|
76
|
+
serial,
|
|
77
|
+
f"input swipe {x} {y} {x} {y} {duration_ms}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def swipe(
|
|
81
|
+
self,
|
|
82
|
+
serial: str,
|
|
83
|
+
x1: int,
|
|
84
|
+
y1: int,
|
|
85
|
+
x2: int,
|
|
86
|
+
y2: int,
|
|
87
|
+
duration_ms: int = 300,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Swipe from (x1,y1) to (x2,y2)."""
|
|
90
|
+
logger.debug(
|
|
91
|
+
"swipe(%s) -> %d,%d to %d,%d (%dms)",
|
|
92
|
+
serial, x1, y1, x2, y2, duration_ms,
|
|
93
|
+
)
|
|
94
|
+
self._adb.shell(
|
|
95
|
+
serial,
|
|
96
|
+
f"input swipe {x1} {y1} {x2} {y2} {duration_ms}",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def drag(
|
|
100
|
+
self,
|
|
101
|
+
serial: str,
|
|
102
|
+
x1: int,
|
|
103
|
+
y1: int,
|
|
104
|
+
x2: int,
|
|
105
|
+
y2: int,
|
|
106
|
+
duration_ms: int = 1000,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Drag from (x1,y1) to (x2,y2) slowly."""
|
|
109
|
+
self.swipe(serial, x1, y1, x2, y2, duration_ms)
|
|
110
|
+
|
|
111
|
+
def scroll_down(self, serial: str, x: int, y: int, amount: int = 500) -> None:
|
|
112
|
+
"""Scroll down by swiping upward from (x,y)."""
|
|
113
|
+
self.swipe(serial, x, y, x, y - amount)
|
|
114
|
+
|
|
115
|
+
def scroll_up(self, serial: str, x: int, y: int, amount: int = 500) -> None:
|
|
116
|
+
"""Scroll up by swiping downward from (x,y)."""
|
|
117
|
+
self.swipe(serial, x, y, x, y + amount)
|
|
118
|
+
|
|
119
|
+
def type_text(self, serial: str, text: str) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Type text on device.
|
|
122
|
+
|
|
123
|
+
Special characters are URL-encoded for safe transmission.
|
|
124
|
+
"""
|
|
125
|
+
logger.debug("type_text(%s) -> %d chars", serial, len(text))
|
|
126
|
+
import urllib.parse
|
|
127
|
+
encoded = urllib.parse.quote(text, safe="")
|
|
128
|
+
self._adb.shell(serial, f"input text '{encoded}'")
|
|
129
|
+
|
|
130
|
+
def paste_clipboard(self, serial: str, text: str) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Set clipboard content on device and paste it.
|
|
133
|
+
|
|
134
|
+
Uses am broadcast to set clipboard, then Ctrl+V to paste.
|
|
135
|
+
"""
|
|
136
|
+
import urllib.parse
|
|
137
|
+
encoded = urllib.parse.quote(text, safe="")
|
|
138
|
+
self._adb.shell(
|
|
139
|
+
serial,
|
|
140
|
+
f"am broadcast -a clipper.set -e text '{encoded}'"
|
|
141
|
+
)
|
|
142
|
+
# Fallback: just type it
|
|
143
|
+
self.type_text(serial, text)
|
|
144
|
+
|
|
145
|
+
def key_event(self, serial: str, keycode: int | KeyCode) -> None:
|
|
146
|
+
"""Send a hardware key event."""
|
|
147
|
+
code = int(keycode)
|
|
148
|
+
logger.debug("key_event(%s) -> %d", serial, code)
|
|
149
|
+
self._adb.shell(serial, f"input keyevent {code}")
|
|
150
|
+
|
|
151
|
+
def home(self, serial: str) -> None:
|
|
152
|
+
"""Press HOME button."""
|
|
153
|
+
self.key_event(serial, KeyCode.KEYCODE_HOME)
|
|
154
|
+
|
|
155
|
+
def back(self, serial: str) -> None:
|
|
156
|
+
"""Press BACK button."""
|
|
157
|
+
self.key_event(serial, KeyCode.KEYCODE_BACK)
|
|
158
|
+
|
|
159
|
+
def app_switch(self, serial: str) -> None:
|
|
160
|
+
"""Press recent apps button."""
|
|
161
|
+
self.key_event(serial, KeyCode.KEYCODE_APP_SWITCH)
|
|
162
|
+
|
|
163
|
+
def power(self, serial: str) -> None:
|
|
164
|
+
"""Press POWER button."""
|
|
165
|
+
self.key_event(serial, KeyCode.KEYCODE_POWER)
|
|
166
|
+
|
|
167
|
+
def volume_up(self, serial: str) -> None:
|
|
168
|
+
"""Press VOLUME UP."""
|
|
169
|
+
self.key_event(serial, KeyCode.KEYCODE_VOLUME_UP)
|
|
170
|
+
|
|
171
|
+
def volume_down(self, serial: str) -> None:
|
|
172
|
+
"""Press VOLUME DOWN."""
|
|
173
|
+
self.key_event(serial, KeyCode.KEYCODE_VOLUME_DOWN)
|
|
174
|
+
|
|
175
|
+
def wake_device(self, serial: str) -> None:
|
|
176
|
+
"""Wake the device screen."""
|
|
177
|
+
self.key_event(serial, KeyCode.KEYCODE_WAKEUP)
|
|
178
|
+
|
|
179
|
+
def sleep_device(self, serial: str) -> None:
|
|
180
|
+
"""Put device screen to sleep."""
|
|
181
|
+
self.key_event(serial, KeyCode.KEYCODE_SLEEP)
|