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,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
|
+
)
|