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,286 @@
1
+ """
2
+ Parses ADB device listing and device property output into typed models.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import re
9
+ from typing import Optional
10
+
11
+ from voidremote.models.device import (
12
+ BatteryInfo,
13
+ ConnectionType,
14
+ CpuInfo,
15
+ Device,
16
+ DeviceCapability,
17
+ DeviceInfo,
18
+ DeviceState,
19
+ NetworkInfo,
20
+ StorageInfo,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Regex for parsing `adb devices -l` output lines
26
+ _DEVICE_LINE_RE = re.compile(
27
+ r"^(?P<serial>\S+)\s+(?P<state>\w+)(?:\s+(?P<attrs>.*))?$"
28
+ )
29
+ _ATTR_RE = re.compile(r"(\w+):(\S+)")
30
+
31
+ # Regex for parsing resolution from `wm size`
32
+ _RESOLUTION_RE = re.compile(r"Physical size:\s*(\d+)x(\d+)")
33
+ # Regex for parsing density from `wm density`
34
+ _DENSITY_RE = re.compile(r"Physical density:\s*(\d+)")
35
+ # Regex for parsing IP from `ip addr show wlan0`
36
+ _IP_RE = re.compile(r"inet\s+([\d.]+)/")
37
+ # Regex for parsing MemTotal from /proc/meminfo
38
+ _MEMTOTAL_RE = re.compile(r"MemTotal:\s+(\d+)\s+kB")
39
+ _MEMAVAIL_RE = re.compile(r"MemAvailable:\s+(\d+)\s+kB")
40
+
41
+
42
+ def parse_devices_output(raw: str) -> list[dict[str, str]]:
43
+ """
44
+ Parse the output of `adb devices -l` into a list of device dicts.
45
+
46
+ Args:
47
+ raw: Raw text output from `adb devices -l`.
48
+
49
+ Returns:
50
+ List of dicts with 'serial', 'state', and parsed attribute keys.
51
+ """
52
+ devices: list[dict[str, str]] = []
53
+ lines = raw.strip().splitlines()
54
+
55
+ for line in lines[1:]: # skip "List of devices attached" header
56
+ line = line.strip()
57
+ if not line or line.startswith("*"):
58
+ continue
59
+ m = _DEVICE_LINE_RE.match(line)
60
+ if not m:
61
+ continue
62
+ entry: dict[str, str] = {
63
+ "serial": m.group("serial"),
64
+ "state": m.group("state"),
65
+ }
66
+ attrs_raw = m.group("attrs") or ""
67
+ for km in _ATTR_RE.finditer(attrs_raw):
68
+ entry[km.group(1)] = km.group(2)
69
+ devices.append(entry)
70
+
71
+ return devices
72
+
73
+
74
+ def state_from_string(state_str: str) -> DeviceState:
75
+ """Map ADB state string to DeviceState enum."""
76
+ mapping: dict[str, DeviceState] = {
77
+ "device": DeviceState.ONLINE,
78
+ "offline": DeviceState.OFFLINE,
79
+ "unauthorized": DeviceState.UNAUTHORIZED,
80
+ "connecting": DeviceState.CONNECTING,
81
+ }
82
+ return mapping.get(state_str.lower(), DeviceState.UNKNOWN)
83
+
84
+
85
+ def connection_type_from_serial(serial: str) -> ConnectionType:
86
+ """Infer connection type from device serial."""
87
+ if re.match(r"^\d+\.\d+\.\d+\.\d+:\d+$", serial):
88
+ return ConnectionType.WIRELESS
89
+ if re.match(r"^emulator-\d+$", serial):
90
+ return ConnectionType.EMULATOR
91
+ return ConnectionType.USB
92
+
93
+
94
+ def parse_battery(raw: str) -> BatteryInfo:
95
+ """Parse output of `dumpsys battery` into BatteryInfo."""
96
+ info = BatteryInfo()
97
+
98
+ def _int(pattern: str) -> int:
99
+ m = re.search(pattern, raw, re.IGNORECASE)
100
+ return int(m.group(1)) if m else 0
101
+
102
+ def _bool(pattern: str) -> bool:
103
+ m = re.search(pattern, raw, re.IGNORECASE)
104
+ if not m:
105
+ return False
106
+ return m.group(1).strip().lower() == "true"
107
+
108
+ info.level = _int(r"level:\s*(\d+)")
109
+ info.is_charging = _bool(r"status:\s*(\w+)") or "charging" in raw.lower()
110
+ info.is_ac_powered = _bool(r"AC powered:\s*(true|false)")
111
+ info.is_usb_powered = _bool(r"USB powered:\s*(true|false)")
112
+ info.voltage = float(_int(r"voltage:\s*(\d+)"))
113
+ info.temperature = float(_int(r"temperature:\s*(\d+)"))
114
+
115
+ health_m = re.search(r"health:\s*(\d+)", raw)
116
+ if health_m:
117
+ health_codes = {1: "unknown", 2: "good", 3: "overheat",
118
+ 4: "dead", 5: "over voltage", 6: "unspecified failure", 7: "cold"}
119
+ info.health = health_codes.get(int(health_m.group(1)), "unknown")
120
+
121
+ tech_m = re.search(r"technology:\s*(\S+)", raw)
122
+ if tech_m:
123
+ info.technology = tech_m.group(1)
124
+
125
+ return info
126
+
127
+
128
+ def parse_storage(raw_df: str) -> StorageInfo:
129
+ """Parse output of `df /data` or similar into StorageInfo."""
130
+ info = StorageInfo()
131
+ lines = raw_df.strip().splitlines()
132
+ for line in lines[1:]:
133
+ parts = line.split()
134
+ if len(parts) >= 6:
135
+ try:
136
+ # Typical: Filesystem 1K-blocks Used Available Use% Mounted
137
+ total_kb = int(parts[1])
138
+ used_kb = int(parts[2])
139
+ avail_kb = int(parts[3])
140
+ mount = parts[5] if len(parts) > 5 else "/data"
141
+ info.total_bytes = total_kb * 1024
142
+ info.used_bytes = used_kb * 1024
143
+ info.available_bytes = avail_kb * 1024
144
+ info.mount_point = mount
145
+ break
146
+ except (ValueError, IndexError):
147
+ continue
148
+ return info
149
+
150
+
151
+ def parse_screen_resolution(wm_size_output: str) -> tuple[int, int]:
152
+ """Parse `wm size` output, return (width, height)."""
153
+ m = _RESOLUTION_RE.search(wm_size_output)
154
+ if m:
155
+ return int(m.group(1)), int(m.group(2))
156
+ return 0, 0
157
+
158
+
159
+ def parse_density(wm_density_output: str) -> int:
160
+ """Parse `wm density` output, return DPI."""
161
+ m = _DENSITY_RE.search(wm_density_output)
162
+ return int(m.group(1)) if m else 0
163
+
164
+
165
+ def parse_ip_address(ip_addr_output: str) -> str:
166
+ """Parse IP from `ip addr show wlan0`."""
167
+ m = _IP_RE.search(ip_addr_output)
168
+ return m.group(1) if m else ""
169
+
170
+
171
+ def parse_meminfo(meminfo: str) -> tuple[int, int]:
172
+ """Parse /proc/meminfo, return (total_bytes, available_bytes)."""
173
+ total_kb = 0
174
+ avail_kb = 0
175
+ tm = _MEMTOTAL_RE.search(meminfo)
176
+ am = _MEMAVAIL_RE.search(meminfo)
177
+ if tm:
178
+ total_kb = int(tm.group(1))
179
+ if am:
180
+ avail_kb = int(am.group(1))
181
+ return total_kb * 1024, avail_kb * 1024
182
+
183
+
184
+ def parse_cpu_abi(getprop_output: str) -> CpuInfo:
185
+ """Build CpuInfo from getprop ro.product.cpu.abi."""
186
+ info = CpuInfo()
187
+ info.abi = getprop_output.strip() or "unknown"
188
+ return info
189
+
190
+
191
+ def build_device_from_raw(
192
+ serial: str,
193
+ state_str: str,
194
+ props: dict[str, str],
195
+ extended_info: Optional[dict[str, str]] = None,
196
+ ) -> Device:
197
+ """
198
+ Construct a Device object from parsed ADB data.
199
+
200
+ Args:
201
+ serial: Device serial.
202
+ state_str: ADB state string.
203
+ props: Dict of getprop values.
204
+ extended_info: Optional additional parsed values.
205
+
206
+ Returns:
207
+ Populated Device instance.
208
+ """
209
+ ext = extended_info or {}
210
+ cpu = CpuInfo(
211
+ abi=props.get("ro.product.cpu.abi", "unknown"),
212
+ abi_list=props.get("ro.product.cpu.abilist", "").split(","),
213
+ )
214
+
215
+ network = NetworkInfo(
216
+ ip_address=ext.get("ip_address", ""),
217
+ wifi_ssid=ext.get("wifi_ssid", ""),
218
+ )
219
+
220
+ battery_raw = ext.get("battery_raw", "")
221
+ battery = parse_battery(battery_raw) if battery_raw else BatteryInfo()
222
+
223
+ storage_raw = ext.get("storage_raw", "")
224
+ storage = parse_storage(storage_raw) if storage_raw else StorageInfo()
225
+
226
+ width, height = 0, 0
227
+ if "resolution" in ext:
228
+ width, height = parse_screen_resolution(ext["resolution"])
229
+
230
+ total_ram, avail_ram = 0, 0
231
+ if "meminfo" in ext:
232
+ total_ram, avail_ram = parse_meminfo(ext["meminfo"])
233
+
234
+ info = DeviceInfo(
235
+ serial=serial,
236
+ model=props.get("ro.product.model", "Unknown"),
237
+ manufacturer=props.get("ro.product.manufacturer", "Unknown"),
238
+ brand=props.get("ro.product.brand", "Unknown"),
239
+ product=props.get("ro.product.name", "Unknown"),
240
+ device_name=props.get("ro.product.device", "Unknown"),
241
+ android_version=props.get("ro.build.version.release", "Unknown"),
242
+ sdk_version=int(props.get("ro.build.version.sdk", "0") or "0"),
243
+ build_id=props.get("ro.build.id", "Unknown"),
244
+ build_fingerprint=props.get("ro.build.fingerprint", ""),
245
+ screen_width=width,
246
+ screen_height=height,
247
+ screen_density=parse_density(ext.get("density", "")),
248
+ battery=battery,
249
+ storage=storage,
250
+ cpu=cpu,
251
+ network=network,
252
+ ram_total_bytes=total_ram,
253
+ ram_available_bytes=avail_ram,
254
+ )
255
+
256
+ conn_type = connection_type_from_serial(serial)
257
+ host, port = "", 5555
258
+ if conn_type == ConnectionType.WIRELESS and ":" in serial:
259
+ parts = serial.rsplit(":", 1)
260
+ host = parts[0]
261
+ try:
262
+ port = int(parts[1])
263
+ except ValueError:
264
+ port = 5555
265
+
266
+ # All online devices get base capabilities
267
+ caps: list[DeviceCapability] = []
268
+ if state_str == "device":
269
+ caps = [
270
+ DeviceCapability.SHELL,
271
+ DeviceCapability.FILE_TRANSFER,
272
+ DeviceCapability.INPUT_CONTROL,
273
+ DeviceCapability.PACKAGE_MANAGER,
274
+ DeviceCapability.MONITORING,
275
+ ]
276
+ if info.sdk_version >= 21:
277
+ caps.append(DeviceCapability.SCREEN_MIRROR)
278
+
279
+ return Device(
280
+ info=info,
281
+ state=state_from_string(state_str),
282
+ connection_type=conn_type,
283
+ host=host,
284
+ port=port,
285
+ capabilities=caps,
286
+ )
@@ -0,0 +1,5 @@
1
+ """VoidRemote CLI package."""
2
+
3
+ from voidremote.cli.main import cli, main
4
+
5
+ __all__ = ["cli", "main"]