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
voidremote/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ VoidRemote - Wireless Android Remote Controller over ADB
3
+ ========================================================
4
+
5
+ A cross-platform desktop application for controlling Android devices
6
+ wirelessly through ADB (Android Debug Bridge).
7
+
8
+ Author: V0IDNETWORK <ilianothingg@gmail.com>
9
+ Website: http://voidNetwork.ir/
10
+ GitHub: https://github.com/V0IDNETWORK/VoidRemote
11
+ License: MIT
12
+ """
13
+
14
+ __version__ = "1.0.0"
15
+ __author__ = "V0IDNETWORK"
16
+ __email__ = "ilianothingg@gmail.com"
17
+ __license__ = "MIT"
18
+ __url__ = "https://github.com/V0IDNETWORK/VoidRemote"
19
+ __description__ = "Wireless Android Remote Controller over ADB"
20
+
21
+ from voidremote.utils.logging import setup_logging
22
+
23
+ __all__ = [
24
+ "__version__",
25
+ "__author__",
26
+ "__email__",
27
+ "__license__",
28
+ "__url__",
29
+ "__description__",
30
+ "setup_logging",
31
+ ]
@@ -0,0 +1,28 @@
1
+ """VoidRemote ADB communication layer."""
2
+
3
+ from voidremote.adb.client import AdbClient, AdbCommandResult, AdbError, AdbNotFoundError
4
+ from voidremote.adb.device_parser import (
5
+ build_device_from_raw,
6
+ parse_battery,
7
+ parse_devices_output,
8
+ parse_ip_address,
9
+ parse_meminfo,
10
+ parse_screen_resolution,
11
+ parse_storage,
12
+ state_from_string,
13
+ )
14
+
15
+ __all__ = [
16
+ "AdbClient",
17
+ "AdbCommandResult",
18
+ "AdbError",
19
+ "AdbNotFoundError",
20
+ "build_device_from_raw",
21
+ "parse_battery",
22
+ "parse_devices_output",
23
+ "parse_ip_address",
24
+ "parse_meminfo",
25
+ "parse_screen_resolution",
26
+ "parse_storage",
27
+ "state_from_string",
28
+ ]
@@ -0,0 +1,385 @@
1
+ """
2
+ ADB client - subprocess-based ADB communication layer.
3
+
4
+ Wraps the system `adb` binary to provide a clean async-friendly API
5
+ for all ADB operations used by VoidRemote.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import shutil
13
+ import subprocess
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ from voidremote.models.device import ConnectionConfig, PairingInfo
19
+ from voidremote.utils.security import (
20
+ sanitize_shell_arg,
21
+ validate_device_path,
22
+ validate_local_path,
23
+ validate_package_name,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class AdbError(Exception):
30
+ """Raised when an ADB operation fails."""
31
+
32
+ def __init__(self, message: str, returncode: int = -1, stderr: str = "") -> None:
33
+ super().__init__(message)
34
+ self.returncode = returncode
35
+ self.stderr = stderr
36
+
37
+
38
+ class AdbNotFoundError(AdbError):
39
+ """Raised when the ADB binary cannot be found."""
40
+
41
+
42
+ class AdbCommandResult:
43
+ """Result of a completed ADB command."""
44
+
45
+ def __init__(self, stdout: str, stderr: str, returncode: int) -> None:
46
+ self.stdout = stdout.strip()
47
+ self.stderr = stderr.strip()
48
+ self.returncode = returncode
49
+
50
+ @property
51
+ def success(self) -> bool:
52
+ return self.returncode == 0
53
+
54
+ def __repr__(self) -> str:
55
+ return (
56
+ f"AdbCommandResult(returncode={self.returncode}, "
57
+ f"stdout={self.stdout[:60]!r})"
58
+ )
59
+
60
+
61
+ class AdbClient:
62
+ """
63
+ Low-level ADB client that wraps the system adb binary.
64
+
65
+ All command execution goes through _run() which handles timeouts,
66
+ retries, and error normalization.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ adb_path: str = "adb",
72
+ default_timeout: float = 30.0,
73
+ server_port: int = 5037,
74
+ ) -> None:
75
+ self._adb_path = adb_path
76
+ self._default_timeout = default_timeout
77
+ self._server_port = server_port
78
+ self._verified = False
79
+
80
+ # ------------------------------------------------------------------
81
+ # Initialization
82
+ # ------------------------------------------------------------------
83
+
84
+ def verify_adb(self) -> str:
85
+ """
86
+ Verify ADB binary is available and return its version string.
87
+
88
+ Raises:
89
+ AdbNotFoundError: If ADB binary not found or not executable.
90
+ """
91
+ resolved = shutil.which(self._adb_path)
92
+ if resolved is None:
93
+ raise AdbNotFoundError(
94
+ f"ADB binary not found: {self._adb_path!r}. "
95
+ "Install Android SDK Platform Tools or specify path in settings."
96
+ )
97
+ result = self._run_sync(["version"])
98
+ self._verified = True
99
+ version_line = result.stdout.split("\n")[0]
100
+ logger.info("ADB available: %s", version_line)
101
+ return version_line
102
+
103
+ def start_server(self) -> None:
104
+ """Start ADB server."""
105
+ self._run_sync(["start-server"])
106
+ logger.debug("ADB server started")
107
+
108
+ def kill_server(self) -> None:
109
+ """Kill ADB server."""
110
+ self._run_sync(["kill-server"])
111
+ logger.debug("ADB server killed")
112
+
113
+ # ------------------------------------------------------------------
114
+ # Device management
115
+ # ------------------------------------------------------------------
116
+
117
+ def list_devices_raw(self) -> str:
118
+ """Return raw output of `adb devices -l`."""
119
+ result = self._run_sync(["devices", "-l"])
120
+ return result.stdout
121
+
122
+ def get_state(self, serial: str) -> str:
123
+ """Return connection state for a device serial."""
124
+ result = self._run_sync(["-s", serial, "get-state"], timeout=5.0)
125
+ return result.stdout.lower()
126
+
127
+ def wait_for_device(self, serial: str, timeout: float = 30.0) -> None:
128
+ """Block until device is online."""
129
+ self._run_sync(["-s", serial, "wait-for-device"], timeout=timeout)
130
+
131
+ # ------------------------------------------------------------------
132
+ # Wireless pairing / connection
133
+ # ------------------------------------------------------------------
134
+
135
+ def pair(self, pairing: PairingInfo) -> AdbCommandResult:
136
+ """Pair with a device using wireless debugging pairing code."""
137
+ address = f"{pairing.host}:{pairing.port}"
138
+ logger.info("Pairing with %s", address)
139
+ result = self._run_sync(
140
+ ["pair", address, pairing.pairing_code],
141
+ timeout=pairing.timeout,
142
+ )
143
+ if "Successfully paired" not in result.stdout and result.returncode != 0:
144
+ raise AdbError(
145
+ f"Pairing failed: {result.stderr or result.stdout}",
146
+ result.returncode,
147
+ )
148
+ return result
149
+
150
+ def connect(self, config: ConnectionConfig) -> AdbCommandResult:
151
+ """Connect to a device over TCP/IP."""
152
+ address = f"{config.host}:{config.port}"
153
+ logger.info("Connecting to %s", address)
154
+ for attempt in range(1, config.retry_count + 1):
155
+ result = self._run_sync(["connect", address], timeout=config.timeout)
156
+ if "connected" in result.stdout.lower():
157
+ logger.info("Connected to %s", address)
158
+ return result
159
+ if attempt < config.retry_count:
160
+ logger.warning(
161
+ "Connect attempt %d/%d failed, retrying in %.1fs",
162
+ attempt, config.retry_count, config.retry_delay,
163
+ )
164
+ time.sleep(config.retry_delay)
165
+ raise AdbError(
166
+ f"Could not connect to {address}: {result.stderr or result.stdout}",
167
+ result.returncode,
168
+ )
169
+
170
+ def disconnect(self, host: str, port: int = 5555) -> AdbCommandResult:
171
+ """Disconnect a wireless device."""
172
+ address = f"{host}:{port}"
173
+ logger.info("Disconnecting from %s", address)
174
+ return self._run_sync(["disconnect", address])
175
+
176
+ def disconnect_all(self) -> AdbCommandResult:
177
+ """Disconnect all wireless devices."""
178
+ return self._run_sync(["disconnect"])
179
+
180
+ def tcpip(self, serial: str, port: int = 5555) -> AdbCommandResult:
181
+ """Switch device to TCP/IP mode on given port."""
182
+ return self._run_sync(["-s", serial, "tcpip", str(port)])
183
+
184
+ # ------------------------------------------------------------------
185
+ # Shell execution
186
+ # ------------------------------------------------------------------
187
+
188
+ def shell(
189
+ self,
190
+ serial: str,
191
+ command: str,
192
+ timeout: Optional[float] = None,
193
+ ) -> AdbCommandResult:
194
+ """Execute a shell command on the device."""
195
+ return self._run_sync(
196
+ ["-s", serial, "shell", command],
197
+ timeout=timeout or self._default_timeout,
198
+ )
199
+
200
+ def shell_raw(
201
+ self,
202
+ serial: str,
203
+ args: list[str],
204
+ timeout: Optional[float] = None,
205
+ ) -> AdbCommandResult:
206
+ """Execute shell with a list of args (each individually quoted)."""
207
+ safe_args = [sanitize_shell_arg(a) for a in args]
208
+ return self._run_sync(
209
+ ["-s", serial, "shell"] + safe_args,
210
+ timeout=timeout or self._default_timeout,
211
+ )
212
+
213
+ # ------------------------------------------------------------------
214
+ # File operations
215
+ # ------------------------------------------------------------------
216
+
217
+ def push(self, serial: str, local: Path, remote: str) -> AdbCommandResult:
218
+ """Push a local file to the device."""
219
+ validated_local = validate_local_path(local)
220
+ validated_remote = validate_device_path(remote)
221
+ if not validated_local.exists():
222
+ raise FileNotFoundError(f"Local file not found: {validated_local}")
223
+ return self._run_sync(
224
+ ["-s", serial, "push", str(validated_local), validated_remote]
225
+ )
226
+
227
+ def pull(self, serial: str, remote: str, local: Path) -> AdbCommandResult:
228
+ """Pull a file from the device to local."""
229
+ validated_remote = validate_device_path(remote)
230
+ local.parent.mkdir(parents=True, exist_ok=True)
231
+ return self._run_sync(
232
+ ["-s", serial, "pull", validated_remote, str(local)]
233
+ )
234
+
235
+ # ------------------------------------------------------------------
236
+ # APK management
237
+ # ------------------------------------------------------------------
238
+
239
+ def install(
240
+ self,
241
+ serial: str,
242
+ apk_path: Path,
243
+ replace: bool = True,
244
+ allow_downgrade: bool = False,
245
+ ) -> AdbCommandResult:
246
+ """Install an APK on the device."""
247
+ validated = validate_local_path(apk_path)
248
+ if not validated.exists():
249
+ raise FileNotFoundError(f"APK not found: {validated}")
250
+ args = ["-s", serial, "install"]
251
+ if replace:
252
+ args.append("-r")
253
+ if allow_downgrade:
254
+ args.append("-d")
255
+ args.append(str(validated))
256
+ return self._run_sync(args, timeout=120.0)
257
+
258
+ def uninstall(
259
+ self,
260
+ serial: str,
261
+ package: str,
262
+ keep_data: bool = False,
263
+ ) -> AdbCommandResult:
264
+ """Uninstall a package from the device."""
265
+ validated_pkg = validate_package_name(package)
266
+ args = ["-s", serial, "uninstall"]
267
+ if keep_data:
268
+ args.append("-k")
269
+ args.append(validated_pkg)
270
+ return self._run_sync(args)
271
+
272
+ # ------------------------------------------------------------------
273
+ # Reboot
274
+ # ------------------------------------------------------------------
275
+
276
+ def reboot(self, serial: str, mode: str = "") -> AdbCommandResult:
277
+ """Reboot device. mode can be '', 'bootloader', or 'recovery'."""
278
+ args = ["-s", serial, "reboot"]
279
+ if mode in ("bootloader", "recovery"):
280
+ args.append(mode)
281
+ return self._run_sync(args)
282
+
283
+ # ------------------------------------------------------------------
284
+ # Screenshots & recording
285
+ # ------------------------------------------------------------------
286
+
287
+ def screenshot(self, serial: str, local_path: Path) -> AdbCommandResult:
288
+ """Capture a screenshot and save it locally."""
289
+ remote_tmp = "/sdcard/void_screenshot.png"
290
+ self.shell(serial, f"screencap -p {remote_tmp}")
291
+ result = self.pull(serial, remote_tmp, local_path)
292
+ self.shell(serial, f"rm {remote_tmp}")
293
+ return result
294
+
295
+ def screenrecord(
296
+ self,
297
+ serial: str,
298
+ remote_path: str = "/sdcard/void_screenrecord.mp4",
299
+ time_limit: int = 180,
300
+ bit_rate: int = 8000000,
301
+ size: Optional[str] = None,
302
+ ) -> subprocess.Popen[bytes]:
303
+ """Start screen recording and return the Popen handle."""
304
+ cmd = [
305
+ self._adb_path, "-s", serial, "shell",
306
+ "screenrecord",
307
+ f"--time-limit={time_limit}",
308
+ f"--bit-rate={bit_rate}",
309
+ ]
310
+ if size:
311
+ cmd += ["--size", size]
312
+ cmd.append(remote_path)
313
+ logger.info("Starting screen record: %s", " ".join(cmd))
314
+ return subprocess.Popen(cmd)
315
+
316
+ # ------------------------------------------------------------------
317
+ # Internal helpers
318
+ # ------------------------------------------------------------------
319
+
320
+ def _build_cmd(self, args: list[str]) -> list[str]:
321
+ """Prefix all commands with adb binary and server port."""
322
+ base = [self._adb_path, "-P", str(self._server_port)]
323
+ return base + args
324
+
325
+ def _run_sync(
326
+ self,
327
+ args: list[str],
328
+ timeout: Optional[float] = None,
329
+ input_data: Optional[bytes] = None,
330
+ ) -> AdbCommandResult:
331
+ """Execute an ADB command synchronously."""
332
+ cmd = self._build_cmd(args)
333
+ effective_timeout = timeout or self._default_timeout
334
+ logger.debug("ADB command: %s", " ".join(cmd))
335
+ try:
336
+ proc = subprocess.run(
337
+ cmd,
338
+ capture_output=True,
339
+ timeout=effective_timeout,
340
+ input=input_data,
341
+ )
342
+ result = AdbCommandResult(
343
+ proc.stdout.decode("utf-8", errors="replace"),
344
+ proc.stderr.decode("utf-8", errors="replace"),
345
+ proc.returncode,
346
+ )
347
+ if result.stderr and result.returncode != 0:
348
+ logger.debug("ADB stderr: %s", result.stderr)
349
+ return result
350
+ except subprocess.TimeoutExpired as exc:
351
+ raise AdbError(
352
+ f"ADB command timed out after {effective_timeout}s: {' '.join(cmd)}"
353
+ ) from exc
354
+ except FileNotFoundError as exc:
355
+ raise AdbNotFoundError(
356
+ f"ADB binary not found: {self._adb_path!r}"
357
+ ) from exc
358
+
359
+ async def _run_async(
360
+ self,
361
+ args: list[str],
362
+ timeout: Optional[float] = None,
363
+ ) -> AdbCommandResult:
364
+ """Execute an ADB command asynchronously."""
365
+ cmd = self._build_cmd(args)
366
+ effective_timeout = timeout or self._default_timeout
367
+ logger.debug("ADB async command: %s", " ".join(cmd))
368
+ try:
369
+ proc = await asyncio.create_subprocess_exec(
370
+ *cmd,
371
+ stdout=asyncio.subprocess.PIPE,
372
+ stderr=asyncio.subprocess.PIPE,
373
+ )
374
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(
375
+ proc.communicate(), timeout=effective_timeout
376
+ )
377
+ return AdbCommandResult(
378
+ stdout_bytes.decode("utf-8", errors="replace"),
379
+ stderr_bytes.decode("utf-8", errors="replace"),
380
+ proc.returncode or 0,
381
+ )
382
+ except asyncio.TimeoutError as exc:
383
+ raise AdbError(
384
+ f"ADB async command timed out after {effective_timeout}s"
385
+ ) from exc