Files
cmautobuy/client/src/settings_ui_event.py
T

1388 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""设置页事件、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)