feat: 实现 Android 设备搜索 (#21)
This commit is contained in:
+158
-11
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user