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,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time device monitoring service.
|
|
3
|
+
|
|
4
|
+
Polls CPU, RAM, battery, temperature, and network stats at regular
|
|
5
|
+
intervals and emits snapshots.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
|
|
18
|
+
from voidremote.adb.client import AdbClient
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DeviceSnapshot:
|
|
25
|
+
"""Point-in-time device performance metrics."""
|
|
26
|
+
|
|
27
|
+
serial: str
|
|
28
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
29
|
+
cpu_usage: float = 0.0
|
|
30
|
+
ram_used_mb: float = 0.0
|
|
31
|
+
ram_total_mb: float = 0.0
|
|
32
|
+
battery_level: int = 0
|
|
33
|
+
battery_temperature: float = 0.0
|
|
34
|
+
battery_is_charging: bool = False
|
|
35
|
+
storage_used_gb: float = 0.0
|
|
36
|
+
storage_total_gb: float = 0.0
|
|
37
|
+
fps: float = 0.0
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def ram_usage_percent(self) -> float:
|
|
41
|
+
if self.ram_total_mb == 0:
|
|
42
|
+
return 0.0
|
|
43
|
+
return (self.ram_used_mb / self.ram_total_mb) * 100.0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def storage_usage_percent(self) -> float:
|
|
47
|
+
if self.storage_total_gb == 0:
|
|
48
|
+
return 0.0
|
|
49
|
+
return (self.storage_used_gb / self.storage_total_gb) * 100.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
SnapshotCallback = Callable[[DeviceSnapshot], None]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MonitorService:
|
|
56
|
+
"""
|
|
57
|
+
Polls device metrics at a configurable interval.
|
|
58
|
+
|
|
59
|
+
Usage::
|
|
60
|
+
|
|
61
|
+
monitor = MonitorService(adb_client)
|
|
62
|
+
monitor.start("serial123", interval=2.0, callback=on_snapshot)
|
|
63
|
+
# ... later ...
|
|
64
|
+
monitor.stop("serial123")
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, adb: AdbClient) -> None:
|
|
68
|
+
self._adb = adb
|
|
69
|
+
self._threads: dict[str, threading.Thread] = {}
|
|
70
|
+
self._stop_events: dict[str, threading.Event] = {}
|
|
71
|
+
self._callbacks: dict[str, list[SnapshotCallback]] = {}
|
|
72
|
+
|
|
73
|
+
def start(
|
|
74
|
+
self,
|
|
75
|
+
serial: str,
|
|
76
|
+
interval: float = 2.0,
|
|
77
|
+
callback: Optional[SnapshotCallback] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Start monitoring a device."""
|
|
80
|
+
if serial in self._threads and self._threads[serial].is_alive():
|
|
81
|
+
logger.warning("Already monitoring %s", serial)
|
|
82
|
+
return
|
|
83
|
+
stop_event = threading.Event()
|
|
84
|
+
self._stop_events[serial] = stop_event
|
|
85
|
+
self._callbacks[serial] = [callback] if callback else []
|
|
86
|
+
|
|
87
|
+
thread = threading.Thread(
|
|
88
|
+
target=self._poll_loop,
|
|
89
|
+
args=(serial, interval, stop_event),
|
|
90
|
+
name=f"monitor-{serial}",
|
|
91
|
+
daemon=True,
|
|
92
|
+
)
|
|
93
|
+
self._threads[serial] = thread
|
|
94
|
+
thread.start()
|
|
95
|
+
logger.info("Started monitoring %s (interval=%.1fs)", serial, interval)
|
|
96
|
+
|
|
97
|
+
def stop(self, serial: str) -> None:
|
|
98
|
+
"""Stop monitoring a device."""
|
|
99
|
+
event = self._stop_events.get(serial)
|
|
100
|
+
if event:
|
|
101
|
+
event.set()
|
|
102
|
+
thread = self._threads.get(serial)
|
|
103
|
+
if thread and thread.is_alive():
|
|
104
|
+
thread.join(timeout=5.0)
|
|
105
|
+
self._threads.pop(serial, None)
|
|
106
|
+
self._stop_events.pop(serial, None)
|
|
107
|
+
self._callbacks.pop(serial, None)
|
|
108
|
+
logger.info("Stopped monitoring %s", serial)
|
|
109
|
+
|
|
110
|
+
def stop_all(self) -> None:
|
|
111
|
+
"""Stop all monitoring threads."""
|
|
112
|
+
for serial in list(self._threads.keys()):
|
|
113
|
+
self.stop(serial)
|
|
114
|
+
|
|
115
|
+
def add_callback(self, serial: str, callback: SnapshotCallback) -> None:
|
|
116
|
+
"""Register an additional callback for a monitored device."""
|
|
117
|
+
if serial not in self._callbacks:
|
|
118
|
+
self._callbacks[serial] = []
|
|
119
|
+
self._callbacks[serial].append(callback)
|
|
120
|
+
|
|
121
|
+
def _poll_loop(
|
|
122
|
+
self, serial: str, interval: float, stop_event: threading.Event
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Main polling loop running in background thread."""
|
|
125
|
+
while not stop_event.is_set():
|
|
126
|
+
try:
|
|
127
|
+
snapshot = self._collect_snapshot(serial)
|
|
128
|
+
for cb in self._callbacks.get(serial, []):
|
|
129
|
+
try:
|
|
130
|
+
cb(snapshot)
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.warning("Monitor callback error: %s", exc)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
logger.debug("Snapshot collection failed for %s: %s", serial, exc)
|
|
135
|
+
stop_event.wait(interval)
|
|
136
|
+
|
|
137
|
+
def _collect_snapshot(self, serial: str) -> DeviceSnapshot:
|
|
138
|
+
"""Collect all metrics for one snapshot."""
|
|
139
|
+
snapshot = DeviceSnapshot(serial=serial)
|
|
140
|
+
|
|
141
|
+
# CPU
|
|
142
|
+
try:
|
|
143
|
+
snapshot.cpu_usage = self._get_cpu_usage(serial)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# RAM
|
|
148
|
+
try:
|
|
149
|
+
total, avail = self._get_ram(serial)
|
|
150
|
+
snapshot.ram_total_mb = total / (1024 * 1024)
|
|
151
|
+
snapshot.ram_used_mb = (total - avail) / (1024 * 1024)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Battery
|
|
156
|
+
try:
|
|
157
|
+
level, temp, charging = self._get_battery(serial)
|
|
158
|
+
snapshot.battery_level = level
|
|
159
|
+
snapshot.battery_temperature = temp
|
|
160
|
+
snapshot.battery_is_charging = charging
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
return snapshot
|
|
165
|
+
|
|
166
|
+
def _get_cpu_usage(self, serial: str) -> float:
|
|
167
|
+
"""Calculate CPU usage percentage from /proc/stat."""
|
|
168
|
+
def read_stat() -> tuple[int, int]:
|
|
169
|
+
out = self._adb.shell(serial, "cat /proc/stat").stdout
|
|
170
|
+
for line in out.splitlines():
|
|
171
|
+
if line.startswith("cpu "):
|
|
172
|
+
nums = [int(x) for x in line.split()[1:]]
|
|
173
|
+
total = sum(nums)
|
|
174
|
+
idle = nums[3] if len(nums) > 3 else 0
|
|
175
|
+
return total, idle
|
|
176
|
+
return 0, 0
|
|
177
|
+
|
|
178
|
+
t1, i1 = read_stat()
|
|
179
|
+
time.sleep(0.2)
|
|
180
|
+
t2, i2 = read_stat()
|
|
181
|
+
dt = t2 - t1
|
|
182
|
+
di = i2 - i1
|
|
183
|
+
if dt == 0:
|
|
184
|
+
return 0.0
|
|
185
|
+
return max(0.0, min(100.0, (1.0 - di / dt) * 100.0))
|
|
186
|
+
|
|
187
|
+
def _get_ram(self, serial: str) -> tuple[int, int]:
|
|
188
|
+
"""Return (total_bytes, available_bytes) from /proc/meminfo."""
|
|
189
|
+
from voidremote.adb.device_parser import parse_meminfo
|
|
190
|
+
out = self._adb.shell(serial, "cat /proc/meminfo").stdout
|
|
191
|
+
return parse_meminfo(out)
|
|
192
|
+
|
|
193
|
+
def _get_battery(self, serial: str) -> tuple[int, float, bool]:
|
|
194
|
+
"""Return (level, temperature_c, is_charging)."""
|
|
195
|
+
from voidremote.adb.device_parser import parse_battery
|
|
196
|
+
out = self._adb.shell(serial, "dumpsys battery").stdout
|
|
197
|
+
info = parse_battery(out)
|
|
198
|
+
return info.level, info.temperature_celsius, info.is_charging
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""VoidRemote GUI package."""
|
voidremote/ui/app.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VoidRemote GUI application entry point.
|
|
3
|
+
|
|
4
|
+
Bootstraps QApplication, applies theme, initializes controller,
|
|
5
|
+
and launches the main window.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
"""Launch the VoidRemote GUI application."""
|
|
19
|
+
# Must import PySide6 before any other Qt symbols
|
|
20
|
+
from PySide6.QtWidgets import QApplication
|
|
21
|
+
from PySide6.QtCore import Qt
|
|
22
|
+
from PySide6.QtGui import QFont
|
|
23
|
+
|
|
24
|
+
from voidremote.config.settings import ensure_dirs, get_settings, LOG_FILE
|
|
25
|
+
from voidremote.utils.logging import setup_logging
|
|
26
|
+
from voidremote.controllers.app_controller import AppController
|
|
27
|
+
from voidremote.ui.theme import apply_dark_theme
|
|
28
|
+
from voidremote.ui.main_window import MainWindow
|
|
29
|
+
|
|
30
|
+
ensure_dirs()
|
|
31
|
+
settings = get_settings()
|
|
32
|
+
|
|
33
|
+
setup_logging(
|
|
34
|
+
level=settings.logging.level,
|
|
35
|
+
log_file=LOG_FILE if settings.logging.file_enabled else None,
|
|
36
|
+
console=settings.logging.console_enabled,
|
|
37
|
+
console_color=settings.logging.console_color,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# High-DPI support
|
|
41
|
+
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
|
42
|
+
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
app = QApplication(sys.argv)
|
|
46
|
+
app.setApplicationName("VoidRemote")
|
|
47
|
+
app.setApplicationDisplayName("VoidRemote")
|
|
48
|
+
app.setApplicationVersion("1.0.0")
|
|
49
|
+
app.setOrganizationName("V0IDNETWORK")
|
|
50
|
+
app.setOrganizationDomain("voidNetwork.ir")
|
|
51
|
+
|
|
52
|
+
# Apply dark theme
|
|
53
|
+
apply_dark_theme(app)
|
|
54
|
+
|
|
55
|
+
# Set default font
|
|
56
|
+
font = QFont(settings.ui.font_family, settings.ui.font_size)
|
|
57
|
+
app.setFont(font)
|
|
58
|
+
|
|
59
|
+
# Create controller (no initialize() yet — done lazily in MainWindow)
|
|
60
|
+
controller = AppController(settings)
|
|
61
|
+
|
|
62
|
+
window = MainWindow(controller)
|
|
63
|
+
window.show()
|
|
64
|
+
|
|
65
|
+
logger.info("VoidRemote GUI started")
|
|
66
|
+
sys.exit(app.exec())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Connect to device dialog."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt, QThread, Signal, Slot
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QDialog,
|
|
10
|
+
QDialogButtonBox,
|
|
11
|
+
QFormLayout,
|
|
12
|
+
QLabel,
|
|
13
|
+
QLineEdit,
|
|
14
|
+
QProgressBar,
|
|
15
|
+
QPushButton,
|
|
16
|
+
QVBoxLayout,
|
|
17
|
+
QWidget,
|
|
18
|
+
QCheckBox,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from voidremote.ui.theme import Colors
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConnectWorker(QThread):
|
|
25
|
+
success = Signal(object)
|
|
26
|
+
failure = Signal(str)
|
|
27
|
+
|
|
28
|
+
def __init__(self, controller: object, host: str, port: int, remember: bool, parent: Optional[object] = None) -> None:
|
|
29
|
+
super().__init__(parent)
|
|
30
|
+
self._controller = controller
|
|
31
|
+
self._host = host
|
|
32
|
+
self._port = port
|
|
33
|
+
self._remember = remember
|
|
34
|
+
|
|
35
|
+
def run(self) -> None:
|
|
36
|
+
try:
|
|
37
|
+
device = self._controller.connect_device(self._host, self._port, self._remember)
|
|
38
|
+
self.success.emit(device)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
self.failure.emit(str(exc))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConnectDialog(QDialog):
|
|
44
|
+
connected = Signal(object)
|
|
45
|
+
|
|
46
|
+
def __init__(self, controller: object, parent: Optional[QWidget] = None) -> None:
|
|
47
|
+
super().__init__(parent)
|
|
48
|
+
self._controller = controller
|
|
49
|
+
self._worker: Optional[ConnectWorker] = None
|
|
50
|
+
self.setWindowTitle("Connect to Device")
|
|
51
|
+
self.setMinimumWidth(400)
|
|
52
|
+
self.setModal(True)
|
|
53
|
+
self._build_ui()
|
|
54
|
+
|
|
55
|
+
def _build_ui(self) -> None:
|
|
56
|
+
layout = QVBoxLayout(self)
|
|
57
|
+
layout.setSpacing(16)
|
|
58
|
+
layout.setContentsMargins(24, 24, 24, 20)
|
|
59
|
+
|
|
60
|
+
title = QLabel("Connect via TCP/IP")
|
|
61
|
+
title.setStyleSheet(f"font-size: 18px; font-weight: 700; color: {Colors.TEXT_PRIMARY};")
|
|
62
|
+
layout.addWidget(title)
|
|
63
|
+
|
|
64
|
+
hint = QLabel(
|
|
65
|
+
"Enter the IP address and port of your Android device.\n"
|
|
66
|
+
"Default ADB port is 5555."
|
|
67
|
+
)
|
|
68
|
+
hint.setStyleSheet(
|
|
69
|
+
f"color: {Colors.TEXT_SECONDARY}; font-size: 12px; "
|
|
70
|
+
f"background: {Colors.BG_ELEVATED}; border-radius: 6px; padding: 10px;"
|
|
71
|
+
)
|
|
72
|
+
hint.setWordWrap(True)
|
|
73
|
+
layout.addWidget(hint)
|
|
74
|
+
|
|
75
|
+
form = QFormLayout()
|
|
76
|
+
form.setSpacing(10)
|
|
77
|
+
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
78
|
+
|
|
79
|
+
self._host_edit = QLineEdit()
|
|
80
|
+
self._host_edit.setPlaceholderText("192.168.1.x")
|
|
81
|
+
form.addRow("IP Address:", self._host_edit)
|
|
82
|
+
|
|
83
|
+
self._port_edit = QLineEdit("5555")
|
|
84
|
+
self._port_edit.setPlaceholderText("5555")
|
|
85
|
+
form.addRow("Port:", self._port_edit)
|
|
86
|
+
|
|
87
|
+
layout.addLayout(form)
|
|
88
|
+
|
|
89
|
+
self._remember_check = QCheckBox("Remember this device")
|
|
90
|
+
self._remember_check.setChecked(True)
|
|
91
|
+
layout.addWidget(self._remember_check)
|
|
92
|
+
|
|
93
|
+
self._progress = QProgressBar()
|
|
94
|
+
self._progress.setRange(0, 0)
|
|
95
|
+
self._progress.hide()
|
|
96
|
+
layout.addWidget(self._progress)
|
|
97
|
+
|
|
98
|
+
self._status = QLabel()
|
|
99
|
+
self._status.setWordWrap(True)
|
|
100
|
+
self._status.hide()
|
|
101
|
+
layout.addWidget(self._status)
|
|
102
|
+
|
|
103
|
+
buttons = QDialogButtonBox()
|
|
104
|
+
self._connect_btn = QPushButton("Connect")
|
|
105
|
+
self._connect_btn.setObjectName("accent")
|
|
106
|
+
self._connect_btn.clicked.connect(self._start_connect)
|
|
107
|
+
buttons.addButton(self._connect_btn, QDialogButtonBox.ButtonRole.AcceptRole)
|
|
108
|
+
cancel = buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
|
109
|
+
cancel.clicked.connect(self.reject)
|
|
110
|
+
layout.addWidget(buttons)
|
|
111
|
+
|
|
112
|
+
def _start_connect(self) -> None:
|
|
113
|
+
host = self._host_edit.text().strip()
|
|
114
|
+
port_str = self._port_edit.text().strip()
|
|
115
|
+
|
|
116
|
+
if not host:
|
|
117
|
+
self._show_error("Enter an IP address.")
|
|
118
|
+
return
|
|
119
|
+
if not port_str.isdigit():
|
|
120
|
+
self._show_error("Port must be a number.")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
self._connect_btn.setEnabled(False)
|
|
124
|
+
self._progress.show()
|
|
125
|
+
self._status.hide()
|
|
126
|
+
|
|
127
|
+
self._worker = ConnectWorker(
|
|
128
|
+
self._controller, host, int(port_str), self._remember_check.isChecked(), self
|
|
129
|
+
)
|
|
130
|
+
self._worker.success.connect(self._on_success)
|
|
131
|
+
self._worker.failure.connect(self._on_failure)
|
|
132
|
+
self._worker.start()
|
|
133
|
+
|
|
134
|
+
@Slot(object)
|
|
135
|
+
def _on_success(self, device: object) -> None:
|
|
136
|
+
self._progress.hide()
|
|
137
|
+
self.connected.emit(device)
|
|
138
|
+
self.accept()
|
|
139
|
+
|
|
140
|
+
@Slot(str)
|
|
141
|
+
def _on_failure(self, msg: str) -> None:
|
|
142
|
+
self._progress.hide()
|
|
143
|
+
self._connect_btn.setEnabled(True)
|
|
144
|
+
self._show_error(msg)
|
|
145
|
+
|
|
146
|
+
def _show_error(self, msg: str) -> None:
|
|
147
|
+
self._status.setText(f"✗ {msg}")
|
|
148
|
+
self._status.setStyleSheet(f"color: {Colors.ERROR};")
|
|
149
|
+
self._status.show()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Wireless ADB pairing dialog."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt, QThread, Signal, Slot
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QDialog,
|
|
10
|
+
QDialogButtonBox,
|
|
11
|
+
QFormLayout,
|
|
12
|
+
QLabel,
|
|
13
|
+
QLineEdit,
|
|
14
|
+
QProgressBar,
|
|
15
|
+
QPushButton,
|
|
16
|
+
QVBoxLayout,
|
|
17
|
+
QWidget,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from voidremote.ui.theme import Colors
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PairWorker(QThread):
|
|
24
|
+
"""Background thread for ADB pairing."""
|
|
25
|
+
|
|
26
|
+
success = Signal(str)
|
|
27
|
+
failure = Signal(str)
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
controller: object,
|
|
32
|
+
host: str,
|
|
33
|
+
port: int,
|
|
34
|
+
code: str,
|
|
35
|
+
parent: Optional[QObject] = None, # type: ignore[name-defined]
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__(parent)
|
|
38
|
+
self._controller = controller
|
|
39
|
+
self._host = host
|
|
40
|
+
self._port = port
|
|
41
|
+
self._code = code
|
|
42
|
+
|
|
43
|
+
def run(self) -> None:
|
|
44
|
+
try:
|
|
45
|
+
ok = self._controller.pair_device(self._host, self._port, self._code)
|
|
46
|
+
if ok:
|
|
47
|
+
self.success.emit(f"Paired with {self._host}:{self._port}")
|
|
48
|
+
else:
|
|
49
|
+
self.failure.emit("Pairing failed — wrong code or timeout")
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
self.failure.emit(str(exc))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PairDialog(QDialog):
|
|
55
|
+
"""
|
|
56
|
+
Dialog for pairing an Android device via Wireless Debugging.
|
|
57
|
+
|
|
58
|
+
Shows setup instructions, input fields, and async pairing progress.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
paired = Signal(str, int) # host, port
|
|
62
|
+
|
|
63
|
+
def __init__(self, controller: object, parent: Optional[QWidget] = None) -> None:
|
|
64
|
+
super().__init__(parent)
|
|
65
|
+
self._controller = controller
|
|
66
|
+
self._worker: Optional[PairWorker] = None
|
|
67
|
+
self.setWindowTitle("Pair New Device")
|
|
68
|
+
self.setMinimumWidth(440)
|
|
69
|
+
self.setModal(True)
|
|
70
|
+
self._build_ui()
|
|
71
|
+
|
|
72
|
+
def _build_ui(self) -> None:
|
|
73
|
+
layout = QVBoxLayout(self)
|
|
74
|
+
layout.setSpacing(16)
|
|
75
|
+
layout.setContentsMargins(24, 24, 24, 20)
|
|
76
|
+
|
|
77
|
+
# Title
|
|
78
|
+
title = QLabel("Pair via Wireless Debugging")
|
|
79
|
+
title.setStyleSheet(
|
|
80
|
+
f"font-size: 18px; font-weight: 700; color: {Colors.TEXT_PRIMARY};"
|
|
81
|
+
)
|
|
82
|
+
layout.addWidget(title)
|
|
83
|
+
|
|
84
|
+
# Instructions
|
|
85
|
+
instructions = QLabel(
|
|
86
|
+
"On your Android device:\n"
|
|
87
|
+
" 1. Go to Settings → Developer Options\n"
|
|
88
|
+
" 2. Enable Wireless Debugging\n"
|
|
89
|
+
" 3. Tap 'Pair device with pairing code'\n"
|
|
90
|
+
" 4. Enter the IP, port, and 6-digit code shown below"
|
|
91
|
+
)
|
|
92
|
+
instructions.setStyleSheet(
|
|
93
|
+
f"color: {Colors.TEXT_SECONDARY}; font-size: 12px; "
|
|
94
|
+
f"background: {Colors.BG_ELEVATED}; border-radius: 6px; padding: 12px;"
|
|
95
|
+
)
|
|
96
|
+
instructions.setWordWrap(True)
|
|
97
|
+
layout.addWidget(instructions)
|
|
98
|
+
|
|
99
|
+
# Form
|
|
100
|
+
form = QFormLayout()
|
|
101
|
+
form.setSpacing(10)
|
|
102
|
+
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
103
|
+
|
|
104
|
+
self._host_edit = QLineEdit()
|
|
105
|
+
self._host_edit.setPlaceholderText("192.168.1.x")
|
|
106
|
+
form.addRow("Device IP:", self._host_edit)
|
|
107
|
+
|
|
108
|
+
self._port_edit = QLineEdit()
|
|
109
|
+
self._port_edit.setPlaceholderText("37001")
|
|
110
|
+
self._port_edit.setText("37001")
|
|
111
|
+
form.addRow("Pairing Port:", self._port_edit)
|
|
112
|
+
|
|
113
|
+
self._code_edit = QLineEdit()
|
|
114
|
+
self._code_edit.setPlaceholderText("123456")
|
|
115
|
+
self._code_edit.setMaxLength(6)
|
|
116
|
+
form.addRow("6-digit Code:", self._code_edit)
|
|
117
|
+
|
|
118
|
+
layout.addLayout(form)
|
|
119
|
+
|
|
120
|
+
# Progress
|
|
121
|
+
self._progress = QProgressBar()
|
|
122
|
+
self._progress.setRange(0, 0)
|
|
123
|
+
self._progress.hide()
|
|
124
|
+
layout.addWidget(self._progress)
|
|
125
|
+
|
|
126
|
+
# Status label
|
|
127
|
+
self._status = QLabel()
|
|
128
|
+
self._status.setWordWrap(True)
|
|
129
|
+
self._status.hide()
|
|
130
|
+
layout.addWidget(self._status)
|
|
131
|
+
|
|
132
|
+
# Buttons
|
|
133
|
+
self._buttons = QDialogButtonBox()
|
|
134
|
+
self._pair_btn = QPushButton("Pair Device")
|
|
135
|
+
self._pair_btn.setObjectName("accent")
|
|
136
|
+
self._pair_btn.clicked.connect(self._start_pairing)
|
|
137
|
+
self._buttons.addButton(self._pair_btn, QDialogButtonBox.ButtonRole.AcceptRole)
|
|
138
|
+
|
|
139
|
+
cancel_btn = self._buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
|
140
|
+
cancel_btn.clicked.connect(self.reject)
|
|
141
|
+
|
|
142
|
+
layout.addWidget(self._buttons)
|
|
143
|
+
|
|
144
|
+
def _start_pairing(self) -> None:
|
|
145
|
+
host = self._host_edit.text().strip()
|
|
146
|
+
port_str = self._port_edit.text().strip()
|
|
147
|
+
code = self._code_edit.text().strip()
|
|
148
|
+
|
|
149
|
+
if not host:
|
|
150
|
+
self._show_error("Enter the device IP address.")
|
|
151
|
+
return
|
|
152
|
+
if not port_str.isdigit():
|
|
153
|
+
self._show_error("Port must be a number.")
|
|
154
|
+
return
|
|
155
|
+
if len(code) != 6 or not code.isdigit():
|
|
156
|
+
self._show_error("Pairing code must be exactly 6 digits.")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
port = int(port_str)
|
|
160
|
+
self._pair_btn.setEnabled(False)
|
|
161
|
+
self._progress.show()
|
|
162
|
+
self._status.hide()
|
|
163
|
+
|
|
164
|
+
self._worker = PairWorker(self._controller, host, port, code, self)
|
|
165
|
+
self._worker.success.connect(self._on_success)
|
|
166
|
+
self._worker.failure.connect(self._on_failure)
|
|
167
|
+
self._worker.start()
|
|
168
|
+
|
|
169
|
+
@Slot(str)
|
|
170
|
+
def _on_success(self, msg: str) -> None:
|
|
171
|
+
self._progress.hide()
|
|
172
|
+
self._status.setText(f"✓ {msg}")
|
|
173
|
+
self._status.setStyleSheet(f"color: {Colors.SUCCESS}; font-weight: 600;")
|
|
174
|
+
self._status.show()
|
|
175
|
+
host = self._host_edit.text().strip()
|
|
176
|
+
port = int(self._port_edit.text().strip())
|
|
177
|
+
self.paired.emit(host, port)
|
|
178
|
+
self.accept()
|
|
179
|
+
|
|
180
|
+
@Slot(str)
|
|
181
|
+
def _on_failure(self, msg: str) -> None:
|
|
182
|
+
self._progress.hide()
|
|
183
|
+
self._pair_btn.setEnabled(True)
|
|
184
|
+
self._show_error(msg)
|
|
185
|
+
|
|
186
|
+
def _show_error(self, msg: str) -> None:
|
|
187
|
+
self._status.setText(f"✗ {msg}")
|
|
188
|
+
self._status.setStyleSheet(f"color: {Colors.ERROR};")
|
|
189
|
+
self._status.show()
|