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.
Files changed (46) hide show
  1. voidremote/__init__.py +31 -0
  2. voidremote/adb/__init__.py +28 -0
  3. voidremote/adb/client.py +385 -0
  4. voidremote/adb/device_parser.py +286 -0
  5. voidremote/cli/__init__.py +5 -0
  6. voidremote/cli/main.py +987 -0
  7. voidremote/config/__init__.py +37 -0
  8. voidremote/config/settings.py +203 -0
  9. voidremote/controllers/__init__.py +5 -0
  10. voidremote/controllers/app_controller.py +222 -0
  11. voidremote/core/__init__.py +1 -0
  12. voidremote/core/automation.py +184 -0
  13. voidremote/models/__init__.py +31 -0
  14. voidremote/models/device.py +263 -0
  15. voidremote/network/__init__.py +1 -0
  16. voidremote/network/discovery.py +116 -0
  17. voidremote/services/__init__.py +13 -0
  18. voidremote/services/device_service.py +340 -0
  19. voidremote/services/input_service.py +181 -0
  20. voidremote/services/monitor_service.py +198 -0
  21. voidremote/ui/__init__.py +1 -0
  22. voidremote/ui/app.py +70 -0
  23. voidremote/ui/dialogs/__init__.py +6 -0
  24. voidremote/ui/dialogs/connect_dialog.py +149 -0
  25. voidremote/ui/dialogs/pair_dialog.py +189 -0
  26. voidremote/ui/main_window.py +371 -0
  27. voidremote/ui/theme.py +529 -0
  28. voidremote/ui/views/__init__.py +15 -0
  29. voidremote/ui/views/dashboard_view.py +233 -0
  30. voidremote/ui/views/files_view.py +305 -0
  31. voidremote/ui/views/monitor_view.py +236 -0
  32. voidremote/ui/views/settings_view.py +257 -0
  33. voidremote/ui/views/shell_view.py +234 -0
  34. voidremote/ui/widgets/__init__.py +14 -0
  35. voidremote/ui/widgets/device_card.py +259 -0
  36. voidremote/ui/widgets/log_view.py +159 -0
  37. voidremote/ui/widgets/metric_gauge.py +90 -0
  38. voidremote/utils/__init__.py +28 -0
  39. voidremote/utils/logging.py +120 -0
  40. voidremote/utils/security.py +216 -0
  41. voidremote-1.0.0.dist-info/METADATA +578 -0
  42. voidremote-1.0.0.dist-info/RECORD +46 -0
  43. voidremote-1.0.0.dist-info/WHEEL +5 -0
  44. voidremote-1.0.0.dist-info/entry_points.txt +5 -0
  45. voidremote-1.0.0.dist-info/licenses/LICENSE +21 -0
  46. 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)