"""设置页事件、ADB 搜索、Client 保存登记和软件更新。 本文件不直接写 SQL。ADB 搜索、SQLite 写入和 Admin HTTP 请求由 Worker 在线程中 执行;页面启动时只同步读取少量索引设置,后台结果通过信号返回主线程更新页面。 """ from typing import Optional from PyQt5.QtCore import ( QCoreApplication, QObject, QThread, QTimer, pyqtSignal, pyqtSlot, ) from .adb_runtime import BundledAdbError, validate_bundled_adb from .android_device_service import ( AndroidDeviceConversionCancelled, AndroidDeviceSearchError, AndroidDeviceSearchResult, AndroidDeviceService, PDD_PACKAGE_NAME, ) from .admin_gateway import ( AdminGatewayError, AndroidDeviceInfo, ClaimCapabilities, ClientInfo, ClientRegistrationGateway, ) from .current_client_service import ( CurrentClientService, generate_client_device_id, load_admin_base_url, ) from .http_admin_gateway import HttpAdminGateway from .selected_android_device_service import SelectedAndroidDeviceService from .settings_repository import SettingsRepository from .settings_ui import AndroidDeviceRow from .task_models import TaskType from .update_ui_event import UpdateUiEventBinder DEVICE_ID_PLACEHOLDER = "待生成" class CurrentClientSaveWorker(QObject): """在线程中先保存本地身份,再登记到 Admin。""" localSaved = pyqtSignal(str, 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, admin_base_url: str, gateway_error: str = "", ): super().__init__() self._service = service self._gateway = gateway self._client_name = client_name self._capabilities = capabilities self._admin_base_url = admin_base_url 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, self._admin_base_url, ) except Exception as exc: self.localSaveFailed.emit(str(exc) or "无法写入本地数据库") return self.localSaved.emit( saved.client_id, saved.client_name, self._admin_base_url, ) 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: restore_warning = "" if self._restore_serial: try: devices = self._service.restore_saved_device( self._restore_serial, lambda: self._cancelled, ) except AndroidDeviceSearchError as exc: restore_warning = str(exc) devices = self._service.search( 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( AndroidDeviceSearchResult(devices, restore_warning) ) finally: self.completed.emit() class PddPackageCheckWorker(QObject): """在后台检查一台 Android 设备是否安装 PDD。""" succeeded = pyqtSignal(str, bool) failed = pyqtSignal(str, str) completed = pyqtSignal(str) def __init__(self, service: AndroidDeviceService, serial: str): super().__init__() self._service = service self._serial = serial self._cancelled = False def cancel(self) -> None: """ADB 命令无法中断;完成后不再发送业务结果。""" self._cancelled = True @pyqtSlot() def run(self) -> None: try: try: installed = self._service.is_package_installed( self._serial, PDD_PACKAGE_NAME, ) except AndroidDeviceSearchError as exc: if not self._cancelled: self.failed.emit(self._serial, str(exc)) except Exception as exc: if not self._cancelled: self.failed.emit( self._serial, str(exc) or "无法检测 PDD 应用", ) else: if not self._cancelled: self.succeeded.emit(self._serial, installed) finally: self.completed.emit(self._serial) 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() convertWifiRequested = pyqtSignal(str) currentDeviceSaveRequested = pyqtSignal(str, str, str) saveRequested = pyqtSignal(str, str) deleteRequested = pyqtSignal(str) androidDeviceConfigurationChanged = pyqtSignal(str) adminBaseUrlChanged = pyqtSignal(str) def __init__( self, page, settings_repository: Optional[SettingsRepository] = None, admin_gateway: Optional[ClientRegistrationGateway] = None, android_device_service: Optional[AndroidDeviceService] = None, update_service=None, update_credential_store=None, live_purchase_adapter_ready: bool = False, ): 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_is_startup_restore = False self._search_thread: Optional[QThread] = None self._search_worker: Optional[AndroidDeviceSearchWorker] = None self._pdd_check_busy = False self._pdd_check_serial = "" self._pending_pdd_check_serial = "" self._pending_android_save_serial = "" self._pdd_check_result_serial = "" self._pdd_is_installed = False self._pdd_check_thread: Optional[QThread] = None self._pdd_check_worker: Optional[PddPackageCheckWorker] = 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._refresh_registration_after_android_setting = False self._live_purchase_adapter_ready = bool( live_purchase_adapter_ready ) self._adb_unavailable_message = "" if android_device_service is None: try: validate_bundled_adb() except BundledAdbError as exc: self._adb_unavailable_message = str(exc) self._android_device_service = ( android_device_service or AndroidDeviceService() ) repository = settings_repository or SettingsRepository() self._settings_repository = repository self.updateEventBinder = UpdateUiEventBinder( page, repository, service=update_service, credential_store=update_credential_store, parent=self, ) self._client_service = CurrentClientService(repository) self._selected_android_device_service = SelectedAndroidDeviceService( repository ) self._admin_gateway = admin_gateway self._owns_admin_gateway = admin_gateway is None self._pending_admin_gateway = None self._gateway_error = "" if self._admin_gateway is None: base_url = load_admin_base_url(repository) 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.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.deviceTableModel.checkedDeviceChanged.connect( self._on_checked_device_changed ) 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() if self._adb_unavailable_message: self._page.deviceStatusLabel.setText( self._adb_unavailable_message ) self._sync_button_state() if self._saved_android_serial and not self._adb_unavailable_message: 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() admin_base_url = self._page.adminBaseUrlInput.text().strip() self._page.deviceNameInput.setText(device_name) self.currentDeviceSaveRequested.emit( device_id, device_name, admin_base_url, ) @pyqtSlot(str, str, str) def _start_save_current_device( self, _displayed_device_id: str, device_name: str, admin_base_url: 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: timeout_value = self._settings_repository.get( "admin.request_timeout_seconds", 3.0 ) timeout_seconds = float(timeout_value) validated_gateway = HttpAdminGateway( admin_base_url, timeout_seconds=timeout_seconds, ) normalized_base_url = admin_base_url.strip().rstrip("/") device = self._selected_android_device() purchase_mode = ( "live" if device is not None and self._live_purchase_adapter_ready else "dry_run" ) supported_types = ( (TaskType.COLLECT, TaskType.PURCHASE) if device is not None and self._live_purchase_adapter_ready else (TaskType.COLLECT,) ) capabilities = ClaimCapabilities( device=device, supported_types=supported_types, purchase_mode=purchase_mode, ) except (TypeError, ValueError) as exc: self._page.set_current_device_status(f"保存失败:{exc}") self._page.adminBaseUrlInput.setFocus() return pending_gateway = ( validated_gateway if self._owns_admin_gateway else self._admin_gateway ) self._pending_admin_gateway = pending_gateway self._page.set_admin_base_url(normalized_base_url) self._current_device_busy = True self._sync_button_state() self._page.set_current_device_status("正在保存本地设备信息…") thread = QThread(self) worker = CurrentClientSaveWorker( self._client_service, pending_gateway, device_name, capabilities, normalized_base_url, "", ) 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: restore_serial = ( self._saved_android_serial if ":" in self._saved_android_serial else "" ) self._start_search_worker(restore_serial) @pyqtSlot() def _request_restore_saved_android_device(self) -> None: if not self._saved_android_serial: return self._start_search_worker( self._saved_android_serial, is_startup_restore=True, ) def _start_search_worker( self, restore_serial: str = "", is_startup_restore: bool = False, ) -> 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._search_is_startup_restore = is_startup_restore self._sync_button_state() if is_startup_restore: status = f"正在恢复已保存设备:{restore_serial}…" elif restore_serial: status = f"正在重新连接已保存 Wi-Fi 设备:{restore_serial}…" else: status = "正在搜索 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(str) def _on_checked_device_changed(self, serial: str) -> None: """勾选变化后刷新按钮,并为最新设备排队检测 PDD。""" self._sync_button_state() if self._closing: return if self._pending_android_save_serial != serial: self._pending_android_save_serial = "" self._pdd_check_result_serial = "" self._pdd_is_installed = False if not serial: self._pending_pdd_check_serial = "" self._page.set_pdd_app_status("未检测") return self._queue_pdd_package_check(serial) def _queue_pdd_package_check(self, serial: str) -> None: """立即检测或只保留最后一次待检测的设备号。""" if self._closing or not serial: return if self._pdd_check_busy: if serial != self._pdd_check_serial: self._pending_pdd_check_serial = serial self._page.set_pdd_app_status("等待检测…") return if ( self._busy or self._current_device_busy or self._search_busy or self._wifi_conversion_busy or self._android_setting_busy ): self._pending_pdd_check_serial = serial self._page.set_pdd_app_status("等待检测…") return self._start_pdd_package_check(serial) def _start_pdd_package_check(self, serial: str) -> None: """启动单次后台包检测;同一时间最多运行一个。""" if self._closing or self._pdd_check_busy: return self._pending_pdd_check_serial = "" self._pdd_check_busy = True self._pdd_check_serial = serial self._page.set_pdd_app_status("正在检测…") self._sync_button_state() thread = QThread(self) worker = PddPackageCheckWorker( self._android_device_service, serial, ) worker.moveToThread(thread) thread.started.connect(worker.run) worker.succeeded.connect(self._on_pdd_package_check_succeeded) worker.failed.connect(self._on_pdd_package_check_failed) worker.completed.connect(thread.quit) worker.completed.connect(worker.deleteLater) thread.finished.connect(self._on_pdd_package_check_finished) thread.finished.connect(thread.deleteLater) self._pdd_check_thread = thread self._pdd_check_worker = worker thread.start() @pyqtSlot(str, bool) def _on_pdd_package_check_succeeded( self, serial: str, installed: bool, ) -> None: if self._closing: return if serial != self._page.deviceTableModel.checked_serial: return self._pdd_check_result_serial = serial self._pdd_is_installed = installed if installed: self._page.set_pdd_app_status( f"{PDD_PACKAGE_NAME}(已安装)" ) return self._page.set_pdd_app_status("未安装,请先在手机安装拼多多") if self._pending_android_save_serial == serial: self._pending_android_save_serial = "" self._page.deviceStatusLabel.setText( "保存失败:所选设备未安装 PDD,请安装后重试" ) @pyqtSlot(str, str) def _on_pdd_package_check_failed( self, serial: str, message: str, ) -> None: if self._closing: return if serial != self._page.deviceTableModel.checked_serial: return self._pdd_check_result_serial = serial self._pdd_is_installed = False self._page.set_pdd_app_status(f"检测失败:{message}") if self._pending_android_save_serial == serial: self._pending_android_save_serial = "" self._page.deviceStatusLabel.setText( f"保存失败:无法确认 PDD 安装状态;{message};原配置未更改" ) @pyqtSlot() def _on_pdd_package_check_finished(self) -> None: finished_serial = self._pdd_check_serial self._pdd_check_worker = None self._pdd_check_thread = None self._pdd_check_busy = False self._pdd_check_serial = "" if self._closing: return current_serial = self._page.deviceTableModel.checked_serial pending_serial = self._pending_pdd_check_serial if pending_serial and pending_serial == current_serial: self._queue_pdd_package_check(pending_serial) return self._pending_pdd_check_serial = "" if ( self._pending_android_save_serial == finished_serial and self._pdd_check_result_serial == finished_serial and self._pdd_is_installed and current_serial == finished_serial ): self._pending_android_save_serial = "" self.saveRequested.emit("", finished_serial) return self._sync_button_state() @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._pending_android_save_serial = serial self._page.deviceStatusLabel.setText( "正在确认所选设备是否安装 PDD…" ) self._queue_pdd_package_check(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) 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._adb_unavailable_message and 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.saveButton.setEnabled( device_commands_enabled and not self._pdd_check_busy 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: displayed_base_url = load_admin_base_url( self._settings_repository ) saved = self._client_service.load() except Exception as exc: self._page.set_current_device_status( f"读取本地设备信息失败:{str(exc) or '数据库不可用'}" ) return self._page.set_admin_base_url(displayed_base_url) 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._selected_android_device_service.load().strip() return AndroidDeviceInfo(serial) if serial else None @pyqtSlot(object) def _on_search_succeeded(self, result: AndroidDeviceSearchResult) -> 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) 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 ) if result.restore_warning: action = ( "自动恢复未完成" if self._search_is_startup_restore else "搜索完成" ) found_text = ( f"找到 {len(rows)} 台 Android 设备" if rows else "未找到其他 Android 设备" ) self._page.deviceStatusLabel.setText( f"{action}:{found_text};已保存设备 " f"{self._saved_android_serial} 恢复失败:" f"{result.restore_warning};配置已保留" ) elif saved_device is not None and saved_device.status == "device": action = ( "恢复完成" if self._search_is_startup_restore else "搜索完成" ) self._page.deviceStatusLabel.setText( f"{action}:找到 {len(rows)} 台 Android 设备;" "已自动选择当前使用设备" ) elif saved_device is not None and saved_device.status == "unauthorized": self._page.deviceStatusLabel.setText( "已重新连接 Wi-Fi 设备,但尚未授权:" f"{self._saved_android_serial};请在手机上允许调试后重试" ) elif saved_device is not None: self._page.deviceStatusLabel.setText( "已保存设备当前不可用:" f"{self._saved_android_serial}({saved_device.status});" "配置已保留" ) 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 调试或无线连接后重试" ) selected_serial = self._page.deviceTableModel.checked_serial if selected_serial: self._queue_pdd_package_check(selected_serial) else: self._page.set_pdd_app_status("未检测") @pyqtSlot(str) def _on_search_failed(self, message: str) -> None: if self._closing: return if self._search_is_startup_restore: self._page.deviceStatusLabel.setText( f"自动恢复失败:{message};已保存配置未更改," "可点击搜索重试" ) elif self._search_restore_serial: self._page.deviceStatusLabel.setText( f"搜索失败:无法重新连接已保存 Wi-Fi 设备;{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 = "" self._search_is_startup_restore = False if not self._closing: self._sync_button_state() pending_serial = self._pending_pdd_check_serial if pending_serial: self._queue_pdd_package_check(pending_serial) @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} 已连接并勾选;" "请确认后点击保存,再拔掉 USB 数据线" ) @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() pending_serial = self._pending_pdd_check_serial if pending_serial: self._queue_pdd_package_check(pending_serial) @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}(已保存)" ) self.androidDeviceConfigurationChanged.emit(serial) else: self._saved_android_serial = "" self._page.deviceTableModel.set_checked_serial("") self._page.deviceStatusLabel.setText( "当前使用设备:未选择(本地配置已删除)" ) self.androidDeviceConfigurationChanged.emit("") displayed_client_id = self._page.deviceIdInput.text().strip() self._refresh_registration_after_android_setting = bool( displayed_client_id and displayed_client_id != DEVICE_ID_PLACEHOLDER ) @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};原设备配置未更改,可重试" ) self._refresh_registration_after_android_setting = False @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() if self._refresh_registration_after_android_setting: self._refresh_registration_after_android_setting = False QTimer.singleShot(0, self._request_save_current_device) @pyqtSlot(str, str, str) def _on_local_saved( self, client_id: str, client_name: str, admin_base_url: str, ) -> None: if self._closing: return self._page.set_client_info(client_id, client_name) self._page.set_admin_base_url(admin_base_url) self._admin_gateway = self._pending_admin_gateway self._gateway_error = "" self.adminBaseUrlChanged.emit(admin_base_url) 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}") self._pending_admin_gateway = None @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 self._pending_admin_gateway = None if not self._closing: self._sync_button_state() @pyqtSlot() def shutdown(self) -> None: """窗口关闭时忽略迟到结果,并等待短请求安全结束。""" if self._closing: return self._closing = True self.updateEventBinder.shutdown() 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: try: search_worker.cancel() signal_slots = ( (search_worker.succeeded, self._on_search_succeeded), (search_worker.failed, self._on_search_failed), ) except RuntimeError: # Worker 已调用 deleteLater,但 finished 清理槽还在消息队列中。 signal_slots = () for signal, slot in signal_slots: try: signal.disconnect(slot) except (TypeError, RuntimeError): pass if search_thread is not None and search_thread.isRunning(): search_thread.quit() search_thread.wait(6000) pdd_worker = self._pdd_check_worker pdd_thread = self._pdd_check_thread if pdd_worker is not None: try: pdd_worker.cancel() signal_slots = ( ( pdd_worker.succeeded, self._on_pdd_package_check_succeeded, ), ( pdd_worker.failed, self._on_pdd_package_check_failed, ), ) except RuntimeError: # Worker 已调用 deleteLater,但 thread.finished 的清理槽 # 还在主线程队列中时,Python 引用可能暂时仍存在。 signal_slots = () for signal, slot in signal_slots: try: signal.disconnect(slot) except (TypeError, RuntimeError): pass if pdd_thread is not None and pdd_thread.isRunning(): pdd_thread.quit() pdd_thread.wait(6000) conversion_worker = self._wifi_conversion_worker conversion_thread = self._wifi_conversion_thread if conversion_worker is not None: try: conversion_worker.cancel() signal_slots = ( ( conversion_worker.progressChanged, self._on_wifi_conversion_progress, ), ( conversion_worker.succeeded, self._on_wifi_conversion_succeeded, ), ( conversion_worker.failed, self._on_wifi_conversion_failed, ), ) except RuntimeError: signal_slots = () for signal, slot in signal_slots: try: signal.disconnect(slot) except (TypeError, RuntimeError): 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: try: setting_worker.cancel() signal_slots = ( ( setting_worker.succeeded, self._on_android_device_setting_succeeded, ), ( setting_worker.failed, self._on_android_device_setting_failed, ), ) except RuntimeError: signal_slots = () for signal, slot in signal_slots: try: signal.disconnect(slot) except (TypeError, RuntimeError): pass if setting_thread is not None and setting_thread.isRunning(): setting_thread.quit() setting_thread.wait(4000)