feat: 实现 Android 设备搜索 (#21)

This commit is contained in:
chengma
2026-08-07 11:53:03 +08:00
parent 4975c6898d
commit 45fde81b8b
5 changed files with 608 additions and 13 deletions
+158 -11
View File
@@ -1,13 +1,17 @@
"""设置页事件、当前 Client 本地保存和后台登记。
"""设置页事件、ADB 搜索、当前 Client 本地保存和后台登记。
本文件不直接写 SQL。SQLite 保存和 Admin HTTP 请求由 Worker 在线程中执行,
后台结果只通过信号返回主线程更新页面。
本文件不直接写 SQL。ADB、SQLite 保存和 Admin HTTP 请求由 Worker 在线程中
执行,后台结果只通过信号返回主线程更新页面。
"""
from typing import Optional
from PyQt5.QtCore import QCoreApplication, QObject, QThread, pyqtSignal, pyqtSlot
from .android_device_service import (
AndroidDeviceSearchError,
AndroidDeviceService,
)
from .admin_gateway import (
AdminGatewayError,
AndroidDeviceInfo,
@@ -21,6 +25,7 @@ from .current_client_service import (
)
from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway
from .settings_repository import SettingsRepository
from .settings_ui import AndroidDeviceRow
DEVICE_ID_PLACEHOLDER = "待生成"
@@ -89,6 +94,41 @@ class CurrentClientSaveWorker(QObject):
self.completed.emit()
class AndroidDeviceSearchWorker(QObject):
"""在后台线程查询 ADB,结果只通过信号返回主线程。"""
succeeded = pyqtSignal(object)
failed = pyqtSignal(str)
completed = pyqtSignal()
def __init__(self, service: AndroidDeviceService):
super().__init__()
self._service = service
self._cancelled = False
def cancel(self) -> None:
"""停止后续属性查询;正在执行的命令等待自身超时结束。"""
self._cancelled = True
@pyqtSlot()
def run(self) -> None:
try:
try:
devices = self._service.search(lambda: self._cancelled)
except AndroidDeviceSearchError as exc:
if not self._cancelled:
self.failed.emit(str(exc))
except Exception as exc:
if not self._cancelled:
self.failed.emit(str(exc) or "无法搜索 Android 设备")
else:
if not self._cancelled:
self.succeeded.emit(devices)
finally:
self.completed.emit()
class SettingsPageEventBinder(QObject):
"""绑定设备管理控件,并向应用层发出稳定事件。"""
@@ -103,6 +143,7 @@ class SettingsPageEventBinder(QObject):
page,
settings_repository: Optional[SettingsRepository] = None,
admin_gateway: Optional[ClientRegistrationGateway] = None,
android_device_service: Optional[AndroidDeviceService] = None,
):
super().__init__(page)
self._page = page
@@ -111,6 +152,12 @@ class SettingsPageEventBinder(QObject):
self._closing = False
self._thread: Optional[QThread] = None
self._worker: Optional[CurrentClientSaveWorker] = None
self._search_busy = False
self._search_thread: Optional[QThread] = None
self._search_worker: Optional[AndroidDeviceSearchWorker] = None
self._android_device_service = (
android_device_service or AndroidDeviceService()
)
repository = settings_repository or SettingsRepository()
self._client_service = CurrentClientService(repository)
@@ -135,6 +182,7 @@ class SettingsPageEventBinder(QObject):
self._start_save_current_device
)
page.searchButton.clicked.connect(self._request_search)
self.searchRequested.connect(self._start_search)
page.connectButton.clicked.connect(self._request_connect)
page.saveButton.clicked.connect(self._request_save)
page.deleteButton.clicked.connect(self._request_delete)
@@ -150,7 +198,7 @@ class SettingsPageEventBinder(QObject):
@pyqtSlot()
def _request_save_current_device(self) -> None:
if self._busy or self._current_device_busy:
if self._busy or self._current_device_busy or self._search_busy:
return
device_id = self._page.deviceIdInput.text().strip()
@@ -162,7 +210,7 @@ class SettingsPageEventBinder(QObject):
def _start_save_current_device(
self, _displayed_device_id: str, device_name: str
) -> None:
if self._closing or self._current_device_busy:
if self._closing or self._current_device_busy or self._search_busy:
return
try:
@@ -202,10 +250,40 @@ class SettingsPageEventBinder(QObject):
@pyqtSlot()
def _request_search(self) -> None:
if self._busy:
if self._busy or self._current_device_busy or self._search_busy:
return
self.searchRequested.emit()
@pyqtSlot()
def _start_search(self) -> None:
if (
self._closing
or self._busy
or self._current_device_busy
or self._search_busy
):
return
self._search_busy = True
self._sync_button_state()
self._page.deviceStatusLabel.setText("正在搜索 Android 设备…")
thread = QThread(self)
worker = AndroidDeviceSearchWorker(self._android_device_service)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.succeeded.connect(self._on_search_succeeded)
worker.failed.connect(self._on_search_failed)
worker.completed.connect(thread.quit)
worker.completed.connect(worker.deleteLater)
thread.finished.connect(self._on_search_thread_finished)
thread.finished.connect(thread.deleteLater)
self._search_thread = thread
self._search_worker = worker
thread.start()
@pyqtSlot()
def _request_connect(self) -> None:
if self._busy:
@@ -251,12 +329,23 @@ class SettingsPageEventBinder(QObject):
serial = self._page.deviceTableModel.checked_serial
has_address = bool(self._page.addressInput.text().strip())
self._page.currentDeviceSaveButton.setEnabled(
not self._busy and not self._current_device_busy
not self._busy
and not self._current_device_busy
and not self._search_busy
)
device_commands_enabled = not self._busy and not self._search_busy
self._page.searchButton.setEnabled(
device_commands_enabled and not self._current_device_busy
)
self._page.connectButton.setEnabled(
device_commands_enabled and has_address
)
self._page.saveButton.setEnabled(
device_commands_enabled and bool(serial)
)
self._page.deleteButton.setEnabled(
device_commands_enabled and bool(serial)
)
self._page.searchButton.setEnabled(not self._busy)
self._page.connectButton.setEnabled(not self._busy and has_address)
self._page.saveButton.setEnabled(not self._busy and bool(serial))
self._page.deleteButton.setEnabled(not self._busy and bool(serial))
def set_busy(self, busy: bool, message: str = "") -> None:
"""切换界面忙碌状态,防止用户重复提交设备命令。"""
@@ -285,6 +374,47 @@ class SettingsPageEventBinder(QObject):
serial = self._page.deviceTableModel.checked_serial.strip()
return AndroidDeviceInfo(serial) if serial else None
@pyqtSlot(object)
def _on_search_succeeded(self, devices) -> None:
if self._closing:
return
rows = [
AndroidDeviceRow(
serial=device.serial,
connection_type=device.connection_type,
model=device.model,
android_version=device.android_version,
status=device.status,
)
for device in devices
]
self._page.set_android_devices(rows)
if rows:
self._page.deviceStatusLabel.setText(
f"搜索完成:找到 {len(rows)} 台 Android 设备"
)
else:
self._page.deviceStatusLabel.setText(
"未找到 Android 设备,请检查 USB 调试或无线连接后重试"
)
@pyqtSlot(str)
def _on_search_failed(self, message: str) -> None:
if self._closing:
return
self._page.deviceStatusLabel.setText(
f"搜索失败:{message};设备列表未更改"
)
@pyqtSlot()
def _on_search_thread_finished(self) -> None:
self._search_worker = None
self._search_thread = None
self._search_busy = False
if not self._closing:
self._sync_button_state()
@pyqtSlot(str, str)
def _on_local_saved(self, client_id: str, client_name: str) -> None:
if self._closing:
@@ -348,3 +478,20 @@ class SettingsPageEventBinder(QObject):
if thread is not None and thread.isRunning():
thread.quit()
thread.wait(4000)
search_worker = self._search_worker
search_thread = self._search_thread
if search_worker is not None:
search_worker.cancel()
for signal, slot in (
(search_worker.succeeded, self._on_search_succeeded),
(search_worker.failed, self._on_search_failed),
):
try:
signal.disconnect(slot)
except TypeError:
pass
if search_thread is not None and search_thread.isRunning():
search_thread.quit()
search_thread.wait(6000)