"""设置页事件、ADB 搜索、当前 Client 本地保存和后台登记。 本文件不直接写 SQL。ADB 搜索、SQLite 写入和 Admin HTTP 请求由 Worker 在线程中 执行;页面启动时只同步读取少量索引设置,后台结果通过信号返回主线程更新页面。 """ from typing import Optional from PyQt5.QtCore import ( QCoreApplication, QObject, QThread, QTimer, pyqtSignal, pyqtSlot, ) from .android_device_service import ( AndroidDeviceConversionCancelled, AndroidDeviceSearchError, AndroidDeviceService, ) from .admin_gateway import ( AdminGatewayError, AndroidDeviceInfo, ClaimCapabilities, ClientInfo, ClientRegistrationGateway, ) from .current_client_service import ( CurrentClientService, generate_client_device_id, ) from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway from .selected_android_device_service import SelectedAndroidDeviceService from .settings_repository import SettingsRepository from .settings_ui import AndroidDeviceRow DEVICE_ID_PLACEHOLDER = "待生成" class CurrentClientSaveWorker(QObject): """在线程中先保存本地身份,再登记到 Admin。""" localSaved = pyqtSignal(str, str) localSaveFailed = pyqtSignal(str) registrationSucceeded = pyqtSignal(str) registrationFailed = pyqtSignal(str) completed = pyqtSignal() def __init__( self, service: CurrentClientService, gateway: Optional[ClientRegistrationGateway], client_name: str, capabilities: ClaimCapabilities, gateway_error: str = "", ): super().__init__() self._service = service self._gateway = gateway self._client_name = client_name self._capabilities = capabilities self._gateway_error = gateway_error self._cancelled = False def cancel(self) -> None: """阻止尚未开始的远端登记;已经发出的 HTTP 等超时返回。""" self._cancelled = True @pyqtSlot() def run(self) -> None: try: try: saved = self._service.save(self._client_name) except Exception as exc: self.localSaveFailed.emit(str(exc) or "无法写入本地数据库") return self.localSaved.emit(saved.client_id, saved.client_name) if self._cancelled: return if self._gateway is None: self.registrationFailed.emit( self._gateway_error or "Admin Gateway 尚未配置" ) return try: receipt = self._gateway.register_client( ClientInfo(saved.client_id, saved.client_name), self._capabilities, ) except AdminGatewayError as exc: self.registrationFailed.emit(str(exc)) except Exception as exc: self.registrationFailed.emit(str(exc) or "Admin 登记失败") else: self.registrationSucceeded.emit(receipt.registered_at) finally: self.completed.emit() class AndroidDeviceSearchWorker(QObject): """在后台搜索设备,也可先恢复一台已保存设备。""" succeeded = pyqtSignal(object) failed = pyqtSignal(str) completed = pyqtSignal() def __init__( self, service: AndroidDeviceService, restore_serial: str = "", ): super().__init__() self._service = service self._restore_serial = restore_serial self._cancelled = False def cancel(self) -> None: """停止后续属性查询;正在执行的命令等待自身超时结束。""" self._cancelled = True @pyqtSlot() def run(self) -> None: try: try: if self._restore_serial: devices = self._service.restore_saved_device( self._restore_serial, lambda: self._cancelled, ) else: 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 AndroidWifiConversionWorker(QObject): """在后台引导一台 USB 设备切换到 Wi-Fi ADB。""" progressChanged = pyqtSignal(str) succeeded = pyqtSignal(object) failed = pyqtSignal(str) completed = pyqtSignal() def __init__(self, service: AndroidDeviceService, usb_serial: str): super().__init__() self._service = service self._usb_serial = usb_serial self._cancelled = False def cancel(self) -> None: """停止轮询和后续命令;正在执行的 ADB 命令等待自身超时。""" self._cancelled = True @pyqtSlot() def run(self) -> None: try: try: result = self._service.convert_usb_to_wifi( self._usb_serial, lambda: self._cancelled, self.progressChanged.emit, ) except AndroidDeviceConversionCancelled: return 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 "无法将 USB 设备转为 Wi-Fi") else: if not self._cancelled: self.succeeded.emit(result) finally: self.completed.emit() class AndroidDeviceSettingWorker(QObject): """在后台保存或删除当前使用的 Android 设备号。""" succeeded = pyqtSignal(str, str) failed = pyqtSignal(str, str) completed = pyqtSignal() def __init__( self, service: SelectedAndroidDeviceService, action: str, serial: str = "", ): super().__init__() self._service = service self._action = action self._serial = serial self._cancelled = False def cancel(self) -> None: """写入无法中断;完成后不再向已关闭页面返回结果。""" self._cancelled = True @pyqtSlot() def run(self) -> None: try: try: if self._action == "save": serial = self._service.save(self._serial) elif self._action == "delete": self._service.delete() serial = "" else: raise ValueError("未知的 Android 设备设置操作") except Exception as exc: if not self._cancelled: self.failed.emit( self._action, str(exc) or "无法写入本地数据库", ) else: if not self._cancelled: self.succeeded.emit(self._action, serial) finally: self.completed.emit() class SettingsPageEventBinder(QObject): """绑定设备管理控件,并向应用层发出稳定事件。""" searchRequested = pyqtSignal() connectRequested = pyqtSignal(str) convertWifiRequested = pyqtSignal(str) currentDeviceSaveRequested = pyqtSignal(str, str) saveRequested = pyqtSignal(str, str) deleteRequested = pyqtSignal(str) def __init__( self, page, settings_repository: Optional[SettingsRepository] = None, admin_gateway: Optional[ClientRegistrationGateway] = None, android_device_service: Optional[AndroidDeviceService] = None, ): super().__init__(page) self._page = page self._busy = False self._current_device_busy = False self._closing = False self._thread: Optional[QThread] = None self._worker: Optional[CurrentClientSaveWorker] = None self._search_busy = False self._search_restore_serial = "" self._search_thread: Optional[QThread] = None self._search_worker: Optional[AndroidDeviceSearchWorker] = None self._wifi_conversion_busy = False self._wifi_conversion_thread: Optional[QThread] = None self._wifi_conversion_worker: Optional[ AndroidWifiConversionWorker ] = None self._android_setting_busy = False self._android_setting_thread: Optional[QThread] = None self._android_setting_worker: Optional[AndroidDeviceSettingWorker] = None self._saved_android_serial = "" self._android_device_service = ( android_device_service or AndroidDeviceService() ) repository = settings_repository or SettingsRepository() self._client_service = CurrentClientService(repository) self._selected_android_device_service = SelectedAndroidDeviceService( repository ) self._admin_gateway = admin_gateway self._gateway_error = "" if self._admin_gateway is None: base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL) timeout_value = repository.get("admin.request_timeout_seconds", 3.0) try: timeout_seconds = float(timeout_value) self._admin_gateway = HttpAdminGateway( base_url if isinstance(base_url, str) else "", timeout_seconds=timeout_seconds, ) except (TypeError, ValueError) as exc: self._gateway_error = str(exc) page.currentDeviceSaveButton.clicked.connect( self._request_save_current_device ) self.currentDeviceSaveRequested.connect( 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.convertWifiButton.clicked.connect(self._request_convert_wifi) self.convertWifiRequested.connect(self._start_convert_wifi) page.saveButton.clicked.connect(self._request_save) self.saveRequested.connect(self._start_save_android_device) page.deleteButton.clicked.connect(self._request_delete) self.deleteRequested.connect(self._start_delete_android_device) page.addressInput.textChanged.connect(self._sync_button_state) page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state) page.destroyed.connect(self.shutdown) application = QCoreApplication.instance() if application is not None: application.aboutToQuit.connect(self.shutdown) self._load_current_client() self._load_selected_android_device() self._sync_button_state() if self._saved_android_serial: QTimer.singleShot(0, self._request_restore_saved_android_device) @pyqtSlot() def _request_save_current_device(self) -> None: if ( self._busy or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return device_id = self._page.deviceIdInput.text().strip() device_name = self._page.deviceNameInput.text().strip() self._page.deviceNameInput.setText(device_name) self.currentDeviceSaveRequested.emit(device_id, device_name) @pyqtSlot(str, str) def _start_save_current_device( self, _displayed_device_id: str, device_name: str ) -> None: if ( self._closing or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return try: device = self._selected_android_device() capabilities = ClaimCapabilities(device=device) except ValueError as exc: self._page.set_current_device_status(f"保存失败:{exc}") return self._current_device_busy = True self._sync_button_state() self._page.set_current_device_status("正在保存本地设备信息…") thread = QThread(self) worker = CurrentClientSaveWorker( self._client_service, self._admin_gateway, device_name, capabilities, self._gateway_error, ) worker.moveToThread(thread) thread.started.connect(worker.run) worker.localSaved.connect(self._on_local_saved) worker.localSaveFailed.connect(self._on_local_save_failed) worker.registrationSucceeded.connect(self._on_registration_succeeded) worker.registrationFailed.connect(self._on_registration_failed) worker.completed.connect(thread.quit) worker.completed.connect(worker.deleteLater) thread.finished.connect(self._on_save_thread_finished) thread.finished.connect(thread.deleteLater) self._thread = thread self._worker = worker thread.start() @pyqtSlot() def _request_search(self) -> None: if ( self._busy or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return self.searchRequested.emit() @pyqtSlot() def _start_search(self) -> None: self._start_search_worker() @pyqtSlot() def _request_restore_saved_android_device(self) -> None: if not self._saved_android_serial: return self._start_search_worker(self._saved_android_serial) def _start_search_worker(self, restore_serial: str = "") -> None: if ( self._closing or self._busy or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return self._search_busy = True self._search_restore_serial = restore_serial self._sync_button_state() status = ( f"正在恢复已保存设备:{restore_serial}…" if restore_serial else "正在搜索 Android 设备…" ) self._page.deviceStatusLabel.setText(status) thread = QThread(self) worker = AndroidDeviceSearchWorker( self._android_device_service, restore_serial, ) 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 or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return address = self._page.addressInput.text().strip() if not address: self._page.deviceStatusLabel.setText( "连接失败:请先填写无线调试地址,例如 192.168.0.173:5555" ) self._page.addressInput.setFocus() return self.connectRequested.emit(address) @pyqtSlot() def _request_convert_wifi(self) -> None: if ( self._busy or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return serial = self._page.deviceTableModel.checked_serial device = self._page.deviceTableModel.device_for_serial(serial) if device is None or device.status != "device": self._page.deviceStatusLabel.setText( "转换失败:请先勾选一台已连接的 USB 设备" ) self._page.deviceTable.setFocus() return if device.connection_type != "USB": self._page.deviceStatusLabel.setText( "转换失败:当前设备已经是 Wi-Fi 连接" ) return self.convertWifiRequested.emit(serial) @pyqtSlot(str) def _start_convert_wifi(self, usb_serial: str) -> None: if ( self._closing or self._busy or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return self._wifi_conversion_busy = True self._sync_button_state() self._page.deviceStatusLabel.setText("正在准备 USB 转 Wi-Fi…") thread = QThread(self) worker = AndroidWifiConversionWorker( self._android_device_service, usb_serial, ) worker.moveToThread(thread) thread.started.connect(worker.run) worker.progressChanged.connect(self._on_wifi_conversion_progress) worker.succeeded.connect(self._on_wifi_conversion_succeeded) worker.failed.connect(self._on_wifi_conversion_failed) worker.completed.connect(thread.quit) worker.completed.connect(worker.deleteLater) thread.finished.connect(self._on_wifi_conversion_finished) thread.finished.connect(thread.deleteLater) self._wifi_conversion_thread = thread self._wifi_conversion_worker = worker thread.start() @pyqtSlot() def _request_save(self) -> None: if ( self._busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return serial = self._page.deviceTableModel.checked_serial if not serial: self._page.deviceStatusLabel.setText("保存失败:请先勾选一台已连接设备") self._page.deviceTable.setFocus() return self.saveRequested.emit("", serial) @pyqtSlot(str, str) def _start_save_android_device( self, _unused_device_name: str, serial: str ) -> None: self._start_android_device_setting("save", serial) @pyqtSlot() def _request_delete(self) -> None: if ( self._busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return serial = self._saved_android_serial if not serial: self._page.deviceStatusLabel.setText("删除失败:当前没有已保存设备") return self.deleteRequested.emit(serial) @pyqtSlot(str) def _start_delete_android_device(self, _serial: str) -> None: self._start_android_device_setting("delete") def _start_android_device_setting( self, action: str, serial: str = "" ) -> None: if ( self._closing or self._busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): return self._android_setting_busy = True self._sync_button_state() message = ( "正在保存当前使用设备…" if action == "save" else "正在删除已保存设备配置…" ) self._page.deviceStatusLabel.setText(message) thread = QThread(self) worker = AndroidDeviceSettingWorker( self._selected_android_device_service, action, serial, ) worker.moveToThread(thread) thread.started.connect(worker.run) worker.succeeded.connect(self._on_android_device_setting_succeeded) worker.failed.connect(self._on_android_device_setting_failed) worker.completed.connect(thread.quit) worker.completed.connect(worker.deleteLater) thread.finished.connect(self._on_android_device_setting_finished) thread.finished.connect(thread.deleteLater) self._android_setting_thread = thread self._android_setting_worker = worker thread.start() @pyqtSlot() def _sync_button_state(self, *_args) -> None: serial = self._page.deviceTableModel.checked_serial selected_device = self._page.deviceTableModel.device_for_serial(serial) has_address = bool(self._page.addressInput.text().strip()) self._page.currentDeviceSaveButton.setEnabled( not self._busy and not self._current_device_busy and not self._search_busy and not self._wifi_conversion_busy and not self._android_setting_busy ) device_commands_enabled = ( not self._busy and not self._search_busy and not self._wifi_conversion_busy and not self._android_setting_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.convertWifiButton.setEnabled( device_commands_enabled and not self._current_device_busy and selected_device is not None and selected_device.status == "device" and selected_device.connection_type == "USB" ) self._page.deleteButton.setEnabled( device_commands_enabled and bool(self._saved_android_serial) ) def set_busy(self, busy: bool, message: str = "") -> None: """切换界面忙碌状态,防止用户重复提交设备命令。""" self._busy = busy self._sync_button_state() if message: self._page.deviceStatusLabel.setText(message) def _load_current_client(self) -> None: """页面创建时恢复本地 Client 信息。""" try: saved = self._client_service.load() except Exception as exc: self._page.set_current_device_status( f"读取本地设备信息失败:{str(exc) or '数据库不可用'}" ) return self._page.set_client_info(saved.client_id, saved.client_name) if saved.client_id: self._page.set_current_device_status("本地设备信息已加载") def _load_selected_android_device(self) -> None: """页面创建时恢复自动化应使用的 Android 设备号。""" try: serial = self._selected_android_device_service.load() except Exception as exc: self._page.deviceStatusLabel.setText( f"读取已保存 Android 设备失败:{str(exc) or '数据库不可用'}" ) return self._saved_android_serial = serial if serial: self._page.deviceStatusLabel.setText( f"当前使用设备:{serial}(等待搜索确认连接)" ) def _selected_android_device(self) -> Optional[AndroidDeviceInfo]: 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) saved_device = self._page.deviceTableModel.device_for_serial( self._saved_android_serial ) if saved_device is not None and saved_device.status == "device": self._page.deviceTableModel.set_checked_serial( self._saved_android_serial ) action = "恢复完成" if self._search_restore_serial else "搜索完成" self._page.deviceStatusLabel.setText( f"{action}:找到 {len(rows)} 台 Android 设备;" "已自动选择当前使用设备" ) elif self._saved_android_serial: self._page.deviceStatusLabel.setText( "已保存设备当前未连接:" f"{self._saved_android_serial};配置已保留" ) elif 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 if self._search_restore_serial: self._page.deviceStatusLabel.setText( f"自动恢复失败:{message};已保存配置未更改," "可点击搜索重试" ) else: 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 self._search_restore_serial = "" if not self._closing: self._sync_button_state() @pyqtSlot(str) def _on_wifi_conversion_progress(self, message: str) -> None: if not self._closing: self._page.deviceStatusLabel.setText(message) @pyqtSlot(object) def _on_wifi_conversion_succeeded(self, result) -> 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 result.devices ] self._page.set_android_devices(rows) self._page.deviceTableModel.set_checked_serial(result.wifi_serial) self._page.deviceStatusLabel.setText( f"Wi-Fi 设备 {result.wifi_serial} 已连接并勾选;" "请确认后点击保存,仅在可信网络使用" ) @pyqtSlot(str) def _on_wifi_conversion_failed(self, message: str) -> None: if self._closing: return self._page.deviceStatusLabel.setText( f"转换失败:{message};设备列表和已保存配置未更改,可重试" ) @pyqtSlot() def _on_wifi_conversion_finished(self) -> None: self._wifi_conversion_worker = None self._wifi_conversion_thread = None self._wifi_conversion_busy = False if not self._closing: self._sync_button_state() @pyqtSlot(str, str) def _on_android_device_setting_succeeded( self, action: str, serial: str ) -> None: if self._closing: return if action == "save": self._saved_android_serial = serial self._page.deviceStatusLabel.setText( f"当前使用设备:{serial}(已保存)" ) else: self._saved_android_serial = "" self._page.deviceTableModel.set_checked_serial("") self._page.deviceStatusLabel.setText( "当前使用设备:未选择(本地配置已删除)" ) @pyqtSlot(str, str) def _on_android_device_setting_failed( self, action: str, message: str ) -> None: if self._closing: return action_text = "保存" if action == "save" else "删除" self._page.deviceStatusLabel.setText( f"{action_text}失败:{message};原设备配置未更改,可重试" ) @pyqtSlot() def _on_android_device_setting_finished(self) -> None: self._android_setting_worker = None self._android_setting_thread = None self._android_setting_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: return self._page.set_client_info(client_id, client_name) self._page.set_current_device_status( "本地已保存,正在登记到 Admin…" ) @pyqtSlot(str) def _on_local_save_failed(self, message: str) -> None: if self._closing: return self._page.set_current_device_status(f"本地保存失败:{message}") @pyqtSlot(str) def _on_registration_succeeded(self, _registered_at: str) -> None: if self._closing: return self._page.set_current_device_status("本地已保存,已登记到 Admin") @pyqtSlot(str) def _on_registration_failed(self, message: str) -> None: if self._closing: return self._page.set_current_device_status( f"本地已保存,Admin 登记失败:{message};可再次点击保存重试" ) @pyqtSlot() def _on_save_thread_finished(self) -> None: self._worker = None self._thread = None self._current_device_busy = False if not self._closing: self._sync_button_state() @pyqtSlot() def shutdown(self) -> None: """窗口关闭时忽略迟到结果,并等待短请求安全结束。""" if self._closing: return self._closing = True worker = self._worker thread = self._thread if worker is not None: worker.cancel() for signal, slot in ( (worker.localSaved, self._on_local_saved), (worker.localSaveFailed, self._on_local_save_failed), (worker.registrationSucceeded, self._on_registration_succeeded), (worker.registrationFailed, self._on_registration_failed), ): try: signal.disconnect(slot) except TypeError: pass 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) conversion_worker = self._wifi_conversion_worker conversion_thread = self._wifi_conversion_thread if conversion_worker is not None: conversion_worker.cancel() for signal, slot in ( ( conversion_worker.progressChanged, self._on_wifi_conversion_progress, ), ( conversion_worker.succeeded, self._on_wifi_conversion_succeeded, ), ( conversion_worker.failed, self._on_wifi_conversion_failed, ), ): try: signal.disconnect(slot) except TypeError: pass if conversion_thread is not None and conversion_thread.isRunning(): conversion_thread.quit() conversion_thread.wait(6000) setting_worker = self._android_setting_worker setting_thread = self._android_setting_thread if setting_worker is not None: setting_worker.cancel() for signal, slot in ( ( setting_worker.succeeded, self._on_android_device_setting_succeeded, ), ( setting_worker.failed, self._on_android_device_setting_failed, ), ): try: signal.disconnect(slot) except TypeError: pass if setting_thread is not None and setting_thread.isRunning(): setting_thread.quit() setting_thread.wait(4000)