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,263 @@
1
+ """
2
+ Device data models for VoidRemote.
3
+
4
+ Defines strongly-typed Pydantic models for Android device representation,
5
+ connection state, and device capabilities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from enum import Enum, auto
13
+ from typing import Optional
14
+
15
+ from pydantic import BaseModel, Field, field_validator
16
+
17
+
18
+ class DeviceState(str, Enum):
19
+ """Connection state of an ADB device."""
20
+
21
+ ONLINE = "online"
22
+ OFFLINE = "offline"
23
+ UNAUTHORIZED = "unauthorized"
24
+ CONNECTING = "connecting"
25
+ PAIRING = "pairing"
26
+ DISCONNECTED = "disconnected"
27
+ UNKNOWN = "unknown"
28
+
29
+
30
+ class ConnectionType(str, Enum):
31
+ """How the device is connected."""
32
+
33
+ USB = "usb"
34
+ WIRELESS = "wireless"
35
+ EMULATOR = "emulator"
36
+
37
+
38
+ class DeviceCapability(str, Enum):
39
+ """Optional capabilities a device may support."""
40
+
41
+ SCREEN_MIRROR = "screen_mirror"
42
+ FILE_TRANSFER = "file_transfer"
43
+ SHELL = "shell"
44
+ INPUT_CONTROL = "input_control"
45
+ PACKAGE_MANAGER = "package_manager"
46
+ MONITORING = "monitoring"
47
+
48
+
49
+ class BatteryInfo(BaseModel):
50
+ """Battery status information."""
51
+
52
+ level: int = Field(default=0, ge=0, le=100, description="Battery percentage 0-100")
53
+ is_charging: bool = Field(default=False)
54
+ is_ac_powered: bool = Field(default=False)
55
+ is_usb_powered: bool = Field(default=False)
56
+ voltage: float = Field(default=0.0, description="Voltage in mV")
57
+ temperature: float = Field(default=0.0, description="Temperature in tenths of degree Celsius")
58
+ health: str = Field(default="unknown")
59
+ technology: str = Field(default="unknown")
60
+
61
+ @property
62
+ def temperature_celsius(self) -> float:
63
+ """Return temperature in Celsius."""
64
+ return self.temperature / 10.0
65
+
66
+ @property
67
+ def voltage_volts(self) -> float:
68
+ """Return voltage in Volts."""
69
+ return self.voltage / 1000.0
70
+
71
+
72
+ class StorageInfo(BaseModel):
73
+ """Storage usage information."""
74
+
75
+ total_bytes: int = Field(default=0, ge=0)
76
+ available_bytes: int = Field(default=0, ge=0)
77
+ used_bytes: int = Field(default=0, ge=0)
78
+ mount_point: str = Field(default="/data")
79
+
80
+ @property
81
+ def total_gb(self) -> float:
82
+ return self.total_bytes / (1024**3)
83
+
84
+ @property
85
+ def available_gb(self) -> float:
86
+ return self.available_bytes / (1024**3)
87
+
88
+ @property
89
+ def used_gb(self) -> float:
90
+ return self.used_bytes / (1024**3)
91
+
92
+ @property
93
+ def usage_percent(self) -> float:
94
+ if self.total_bytes == 0:
95
+ return 0.0
96
+ return (self.used_bytes / self.total_bytes) * 100.0
97
+
98
+
99
+ class CpuInfo(BaseModel):
100
+ """CPU information."""
101
+
102
+ abi: str = Field(default="unknown", description="CPU ABI (e.g. arm64-v8a)")
103
+ abi_list: list[str] = Field(default_factory=list)
104
+ cores: int = Field(default=0, ge=0)
105
+ governor: str = Field(default="unknown")
106
+ usage_percent: float = Field(default=0.0, ge=0.0, le=100.0)
107
+ frequency_mhz: float = Field(default=0.0, ge=0.0)
108
+
109
+
110
+ class NetworkInfo(BaseModel):
111
+ """Network connection information."""
112
+
113
+ ip_address: str = Field(default="")
114
+ wifi_ssid: str = Field(default="")
115
+ wifi_signal: int = Field(default=0, description="WiFi signal strength in dBm")
116
+ mac_address: str = Field(default="")
117
+ interface: str = Field(default="wlan0")
118
+
119
+
120
+ class DeviceInfo(BaseModel):
121
+ """Complete device information."""
122
+
123
+ serial: str = Field(description="ADB serial identifier")
124
+ model: str = Field(default="Unknown")
125
+ manufacturer: str = Field(default="Unknown")
126
+ brand: str = Field(default="Unknown")
127
+ product: str = Field(default="Unknown")
128
+ device_name: str = Field(default="Unknown")
129
+ android_version: str = Field(default="Unknown")
130
+ sdk_version: int = Field(default=0)
131
+ build_id: str = Field(default="Unknown")
132
+ build_fingerprint: str = Field(default="")
133
+ screen_width: int = Field(default=0, ge=0)
134
+ screen_height: int = Field(default=0, ge=0)
135
+ screen_density: int = Field(default=0, ge=0, description="DPI")
136
+ battery: BatteryInfo = Field(default_factory=BatteryInfo)
137
+ storage: StorageInfo = Field(default_factory=StorageInfo)
138
+ cpu: CpuInfo = Field(default_factory=CpuInfo)
139
+ network: NetworkInfo = Field(default_factory=NetworkInfo)
140
+ ram_total_bytes: int = Field(default=0, ge=0)
141
+ ram_available_bytes: int = Field(default=0, ge=0)
142
+ features: list[str] = Field(default_factory=list)
143
+
144
+ @field_validator("serial")
145
+ @classmethod
146
+ def validate_serial(cls, v: str) -> str:
147
+ """Ensure serial is not empty."""
148
+ if not v.strip():
149
+ raise ValueError("Device serial cannot be empty")
150
+ return v.strip()
151
+
152
+ @property
153
+ def display_name(self) -> str:
154
+ """Human-readable device name."""
155
+ if self.model and self.model != "Unknown":
156
+ return f"{self.manufacturer} {self.model}".strip()
157
+ return self.serial
158
+
159
+ @property
160
+ def screen_resolution(self) -> str:
161
+ """Screen resolution as string."""
162
+ if self.screen_width and self.screen_height:
163
+ return f"{self.screen_width}x{self.screen_height}"
164
+ return "Unknown"
165
+
166
+ @property
167
+ def ram_total_gb(self) -> float:
168
+ return self.ram_total_bytes / (1024**3)
169
+
170
+ @property
171
+ def ram_available_gb(self) -> float:
172
+ return self.ram_available_bytes / (1024**3)
173
+
174
+
175
+ class Device(BaseModel):
176
+ """A connected ADB device with full state."""
177
+
178
+ info: DeviceInfo
179
+ state: DeviceState = Field(default=DeviceState.UNKNOWN)
180
+ connection_type: ConnectionType = Field(default=ConnectionType.USB)
181
+ host: str = Field(default="")
182
+ port: int = Field(default=5555, ge=1, le=65535)
183
+ is_trusted: bool = Field(default=False)
184
+ connected_at: Optional[datetime] = Field(default=None)
185
+ last_seen: Optional[datetime] = Field(default=None)
186
+ capabilities: list[DeviceCapability] = Field(default_factory=list)
187
+ tags: list[str] = Field(default_factory=list)
188
+ alias: Optional[str] = Field(default=None)
189
+
190
+ @property
191
+ def serial(self) -> str:
192
+ return self.info.serial
193
+
194
+ @property
195
+ def display_name(self) -> str:
196
+ return self.alias or self.info.display_name
197
+
198
+ @property
199
+ def is_connected(self) -> bool:
200
+ return self.state == DeviceState.ONLINE
201
+
202
+ @property
203
+ def adb_address(self) -> str:
204
+ """Return ADB network address for wireless devices."""
205
+ if self.connection_type == ConnectionType.WIRELESS and self.host:
206
+ return f"{self.host}:{self.port}"
207
+ return self.serial
208
+
209
+ def has_capability(self, cap: DeviceCapability) -> bool:
210
+ """Check if device supports a capability."""
211
+ return cap in self.capabilities
212
+
213
+ class Config:
214
+ use_enum_values = False
215
+
216
+
217
+ @dataclass
218
+ class PairingInfo:
219
+ """Information needed to pair a device over wireless debugging."""
220
+
221
+ host: str
222
+ port: int
223
+ pairing_code: str
224
+ timeout: float = 30.0
225
+
226
+ def __post_init__(self) -> None:
227
+ if not self.host:
228
+ raise ValueError("Host cannot be empty")
229
+ if not (1 <= self.port <= 65535):
230
+ raise ValueError(f"Invalid port: {self.port}")
231
+ if not self.pairing_code:
232
+ raise ValueError("Pairing code cannot be empty")
233
+
234
+
235
+ @dataclass
236
+ class ConnectionConfig:
237
+ """Configuration for an ADB connection."""
238
+
239
+ host: str
240
+ port: int = 5555
241
+ timeout: float = 10.0
242
+ retry_count: int = 3
243
+ retry_delay: float = 1.0
244
+ keep_alive: bool = True
245
+
246
+ def __post_init__(self) -> None:
247
+ if not self.host:
248
+ raise ValueError("Host cannot be empty")
249
+ if not (1 <= self.port <= 65535):
250
+ raise ValueError(f"Invalid port: {self.port}")
251
+
252
+
253
+ @dataclass
254
+ class TrustedDevice:
255
+ """A remembered/trusted device for auto-reconnect."""
256
+
257
+ serial: str
258
+ host: str
259
+ port: int
260
+ alias: Optional[str] = None
261
+ last_connected: Optional[datetime] = None
262
+ auto_connect: bool = True
263
+ tags: list[str] = field(default_factory=list)
@@ -0,0 +1 @@
1
+ """VoidRemote network utilities."""
@@ -0,0 +1,116 @@
1
+ """
2
+ Network device discovery utilities.
3
+
4
+ Scans the local subnet for Android devices with ADB enabled
5
+ using TCP port scanning on common ADB ports.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ipaddress
11
+ import logging
12
+ import socket
13
+ import threading
14
+ from typing import Callable, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ ADB_PORTS = [5555, 5556, 5557, 5558, 5559]
19
+ DEFAULT_TIMEOUT = 0.5
20
+
21
+
22
+ def _check_port(host: str, port: int, timeout: float = DEFAULT_TIMEOUT) -> bool:
23
+ """Return True if TCP port is open on host."""
24
+ try:
25
+ with socket.create_connection((host, port), timeout=timeout):
26
+ return True
27
+ except (OSError, ConnectionRefusedError, TimeoutError):
28
+ return False
29
+
30
+
31
+ def scan_host(host: str, ports: list[int] = ADB_PORTS) -> list[tuple[str, int]]:
32
+ """
33
+ Scan a single host for open ADB ports.
34
+
35
+ Returns:
36
+ List of (host, port) tuples where ADB appears to be listening.
37
+ """
38
+ results: list[tuple[str, int]] = []
39
+ for port in ports:
40
+ if _check_port(host, port):
41
+ results.append((host, port))
42
+ return results
43
+
44
+
45
+ def scan_subnet(
46
+ subnet: str,
47
+ ports: list[int] = ADB_PORTS,
48
+ timeout: float = DEFAULT_TIMEOUT,
49
+ max_workers: int = 64,
50
+ on_found: Optional[Callable[[str, int], None]] = None,
51
+ on_progress: Optional[Callable[[int, int], None]] = None,
52
+ ) -> list[tuple[str, int]]:
53
+ """
54
+ Scan an entire subnet for ADB devices.
55
+
56
+ Args:
57
+ subnet: CIDR notation subnet (e.g. "192.168.1.0/24").
58
+ ports: List of ports to check.
59
+ timeout: Per-connection timeout in seconds.
60
+ max_workers: Maximum concurrent scan threads.
61
+ on_found: Optional callback called when a device is discovered.
62
+ on_progress: Optional callback(scanned, total) for progress.
63
+
64
+ Returns:
65
+ List of (host, port) tuples with open ADB ports.
66
+ """
67
+ try:
68
+ network = ipaddress.ip_network(subnet, strict=False)
69
+ except ValueError as exc:
70
+ raise ValueError(f"Invalid subnet: {subnet!r}") from exc
71
+
72
+ hosts = list(network.hosts())
73
+ total = len(hosts)
74
+ results: list[tuple[str, int]] = []
75
+ scanned = 0
76
+ lock = threading.Lock()
77
+ semaphore = threading.Semaphore(max_workers)
78
+
79
+ def scan_one(host: str) -> None:
80
+ nonlocal scanned
81
+ with semaphore:
82
+ found = scan_host(host, ports)
83
+ with lock:
84
+ scanned += 1
85
+ results.extend(found)
86
+ if on_found and found:
87
+ for h, p in found:
88
+ on_found(h, p)
89
+ if on_progress:
90
+ on_progress(scanned, total)
91
+
92
+ threads = [threading.Thread(target=scan_one, args=(str(h),), daemon=True) for h in hosts]
93
+ for t in threads:
94
+ t.start()
95
+ for t in threads:
96
+ t.join()
97
+
98
+ logger.info("Subnet scan %s complete: %d hosts scanned, %d found", subnet, total, len(results))
99
+ return results
100
+
101
+
102
+ def get_local_subnet() -> Optional[str]:
103
+ """
104
+ Attempt to determine the local network subnet.
105
+
106
+ Returns:
107
+ CIDR string like "192.168.1.0/24", or None if not determinable.
108
+ """
109
+ try:
110
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
111
+ s.connect(("8.8.8.8", 80))
112
+ local_ip = s.getsockname()[0]
113
+ parts = local_ip.split(".")
114
+ return f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"
115
+ except Exception:
116
+ return None
@@ -0,0 +1,13 @@
1
+ """VoidRemote service layer."""
2
+
3
+ from voidremote.services.device_service import DeviceService
4
+ from voidremote.services.input_service import InputService, KeyCode
5
+ from voidremote.services.monitor_service import DeviceSnapshot, MonitorService
6
+
7
+ __all__ = [
8
+ "DeviceService",
9
+ "DeviceSnapshot",
10
+ "InputService",
11
+ "KeyCode",
12
+ "MonitorService",
13
+ ]