feat: 检测 Android 设备 PDD 安装状态 (#29)
This commit is contained in:
@@ -10,6 +10,7 @@ from typing import Callable, List, Optional, Sequence
|
|||||||
DEFAULT_ADB_TIMEOUT_SECONDS = 5.0
|
DEFAULT_ADB_TIMEOUT_SECONDS = 5.0
|
||||||
DEFAULT_DEVICE_READY_CHECKS = 8
|
DEFAULT_DEVICE_READY_CHECKS = 8
|
||||||
DEFAULT_DEVICE_READY_INTERVAL_SECONDS = 0.25
|
DEFAULT_DEVICE_READY_INTERVAL_SECONDS = 0.25
|
||||||
|
PDD_PACKAGE_NAME = "com.xunmeng.pinduoduo"
|
||||||
|
|
||||||
|
|
||||||
class AndroidDeviceSearchError(RuntimeError):
|
class AndroidDeviceSearchError(RuntimeError):
|
||||||
@@ -111,6 +112,26 @@ class AndroidDeviceService:
|
|||||||
)
|
)
|
||||||
return enriched
|
return enriched
|
||||||
|
|
||||||
|
def is_package_installed(
|
||||||
|
self,
|
||||||
|
serial: str,
|
||||||
|
package_name: str = PDD_PACKAGE_NAME,
|
||||||
|
) -> bool:
|
||||||
|
"""查询指定设备是否安装目标包;ADB 失败时抛出可读异常。"""
|
||||||
|
|
||||||
|
self._validate_saved_serial(serial)
|
||||||
|
if not package_name or any(character.isspace() for character in package_name):
|
||||||
|
raise AndroidDeviceSearchError("应用包名格式不正确")
|
||||||
|
|
||||||
|
result = self._run(
|
||||||
|
["adb", "-s", serial, "shell", "pm", "path", package_name],
|
||||||
|
"检测 PDD 应用",
|
||||||
|
)
|
||||||
|
return any(
|
||||||
|
line.strip().startswith("package:")
|
||||||
|
for line in (result.stdout or "").splitlines()
|
||||||
|
)
|
||||||
|
|
||||||
def restore_saved_device(
|
def restore_saved_device(
|
||||||
self,
|
self,
|
||||||
serial: str,
|
serial: str,
|
||||||
|
|||||||
+29
-18
@@ -240,16 +240,11 @@ class SettingsPage(QWidget):
|
|||||||
"设备信息尚未保存", self
|
"设备信息尚未保存", self
|
||||||
)
|
)
|
||||||
self.currentDeviceStatusLabel.setAccessibleName("当前设备保存状态")
|
self.currentDeviceStatusLabel.setAccessibleName("当前设备保存状态")
|
||||||
|
self.currentDeviceStatusLabel.setWordWrap(True)
|
||||||
|
|
||||||
self.currentDeviceCard = self._build_current_device_card()
|
self.currentDeviceCard = self._build_current_device_card()
|
||||||
|
|
||||||
self.addressInput = LineEdit(self)
|
|
||||||
self.addressInput.setPlaceholderText("例如 192.168.0.173:5555")
|
|
||||||
self.addressInput.setClearButtonEnabled(True)
|
|
||||||
self.addressInput.setAccessibleName("Android 无线调试地址")
|
|
||||||
|
|
||||||
self.searchButton = PushButton(FIF.SEARCH, "搜索", self)
|
self.searchButton = PushButton(FIF.SEARCH, "搜索", self)
|
||||||
self.connectButton = PushButton(FIF.LINK, "连接", self)
|
|
||||||
self.convertWifiButton = PushButton(FIF.WIFI, "转为 Wi-Fi", self)
|
self.convertWifiButton = PushButton(FIF.WIFI, "转为 Wi-Fi", self)
|
||||||
self.convertWifiButton.setAccessibleName(
|
self.convertWifiButton.setAccessibleName(
|
||||||
"将勾选的 USB Android 设备转为 Wi-Fi 连接"
|
"将勾选的 USB Android 设备转为 Wi-Fi 连接"
|
||||||
@@ -288,6 +283,10 @@ class SettingsPage(QWidget):
|
|||||||
|
|
||||||
self.deviceStatusLabel = CaptionLabel("当前使用设备:未选择", self)
|
self.deviceStatusLabel = CaptionLabel("当前使用设备:未选择", self)
|
||||||
self.deviceStatusLabel.setAccessibleName("Android 设备搜索和选择状态")
|
self.deviceStatusLabel.setAccessibleName("Android 设备搜索和选择状态")
|
||||||
|
self.deviceStatusLabel.setWordWrap(True)
|
||||||
|
self.pddAppStatusLabel = CaptionLabel("未检测", self)
|
||||||
|
self.pddAppStatusLabel.setAccessibleName("PDD 应用安装状态")
|
||||||
|
self.pddAppStatusLabel.setWordWrap(True)
|
||||||
self.androidDeviceCard = self._build_android_device_card()
|
self.androidDeviceCard = self._build_android_device_card()
|
||||||
|
|
||||||
content = QWidget(self)
|
content = QWidget(self)
|
||||||
@@ -329,7 +328,13 @@ class SettingsPage(QWidget):
|
|||||||
layout = QVBoxLayout(card)
|
layout = QVBoxLayout(card)
|
||||||
layout.setContentsMargins(24, 20, 24, 22)
|
layout.setContentsMargins(24, 20, 24, 22)
|
||||||
layout.setSpacing(12)
|
layout.setSpacing(12)
|
||||||
layout.addWidget(SubtitleLabel("当前设备", card))
|
|
||||||
|
titleLayout = QHBoxLayout()
|
||||||
|
titleLayout.setSpacing(12)
|
||||||
|
titleLayout.addWidget(SubtitleLabel("当前设备", card))
|
||||||
|
titleLayout.addStretch(1)
|
||||||
|
titleLayout.addWidget(self.currentDeviceStatusLabel, 1)
|
||||||
|
layout.addLayout(titleLayout)
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
form.setHorizontalSpacing(16)
|
form.setHorizontalSpacing(16)
|
||||||
@@ -346,7 +351,6 @@ class SettingsPage(QWidget):
|
|||||||
commandLayout.addStretch(1)
|
commandLayout.addStretch(1)
|
||||||
commandLayout.addWidget(self.currentDeviceSaveButton)
|
commandLayout.addWidget(self.currentDeviceSaveButton)
|
||||||
layout.addLayout(commandLayout)
|
layout.addLayout(commandLayout)
|
||||||
layout.addWidget(self.currentDeviceStatusLabel)
|
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def _build_android_device_card(self) -> CardWidget:
|
def _build_android_device_card(self) -> CardWidget:
|
||||||
@@ -354,16 +358,19 @@ class SettingsPage(QWidget):
|
|||||||
layout = QVBoxLayout(card)
|
layout = QVBoxLayout(card)
|
||||||
layout.setContentsMargins(24, 20, 24, 22)
|
layout.setContentsMargins(24, 20, 24, 22)
|
||||||
layout.setSpacing(12)
|
layout.setSpacing(12)
|
||||||
layout.addWidget(SubtitleLabel("Android 设备", card))
|
|
||||||
|
|
||||||
addressLayout = QHBoxLayout()
|
titleLayout = QHBoxLayout()
|
||||||
addressLayout.setSpacing(12)
|
titleLayout.setSpacing(12)
|
||||||
addressLabel = CaptionLabel("无线地址", card)
|
titleLayout.addWidget(SubtitleLabel("Android 设备", card))
|
||||||
addressLabel.setBuddy(self.addressInput)
|
titleLayout.addStretch(1)
|
||||||
addressLayout.addWidget(addressLabel)
|
titleLayout.addWidget(self.deviceStatusLabel, 1)
|
||||||
addressLayout.addWidget(self.addressInput, 1)
|
layout.addLayout(titleLayout)
|
||||||
addressLayout.addWidget(self.connectButton)
|
|
||||||
layout.addLayout(addressLayout)
|
pddStatusLayout = QHBoxLayout()
|
||||||
|
pddStatusLayout.setSpacing(12)
|
||||||
|
pddStatusLayout.addWidget(CaptionLabel("PDD 应用", card))
|
||||||
|
pddStatusLayout.addWidget(self.pddAppStatusLabel, 1)
|
||||||
|
layout.addLayout(pddStatusLayout)
|
||||||
|
|
||||||
commandLayout = QHBoxLayout()
|
commandLayout = QHBoxLayout()
|
||||||
commandLayout.setSpacing(10)
|
commandLayout.setSpacing(10)
|
||||||
@@ -374,7 +381,6 @@ class SettingsPage(QWidget):
|
|||||||
commandLayout.addWidget(self.deleteButton)
|
commandLayout.addWidget(self.deleteButton)
|
||||||
layout.addLayout(commandLayout)
|
layout.addLayout(commandLayout)
|
||||||
layout.addWidget(self.deviceTable, 1)
|
layout.addWidget(self.deviceTable, 1)
|
||||||
layout.addWidget(self.deviceStatusLabel)
|
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def set_client_info(self, device_id: str, device_name: str) -> None:
|
def set_client_info(self, device_id: str, device_name: str) -> None:
|
||||||
@@ -394,6 +400,11 @@ class SettingsPage(QWidget):
|
|||||||
self.deviceTableModel.set_devices(devices)
|
self.deviceTableModel.set_devices(devices)
|
||||||
self._update_device_placeholder_rows()
|
self._update_device_placeholder_rows()
|
||||||
|
|
||||||
|
def set_pdd_app_status(self, message: str) -> None:
|
||||||
|
"""显示当前勾选设备上的 PDD 安装检测结果。"""
|
||||||
|
|
||||||
|
self.pddAppStatusLabel.setText(message)
|
||||||
|
|
||||||
def set_saved_android_device(self, serial: str) -> None:
|
def set_saved_android_device(self, serial: str) -> None:
|
||||||
"""显示已经保存并实际用于自动化的 Android 设备。"""
|
"""显示已经保存并实际用于自动化的 Android 设备。"""
|
||||||
|
|
||||||
|
|||||||
+259
-27
@@ -19,6 +19,7 @@ from .android_device_service import (
|
|||||||
AndroidDeviceConversionCancelled,
|
AndroidDeviceConversionCancelled,
|
||||||
AndroidDeviceSearchError,
|
AndroidDeviceSearchError,
|
||||||
AndroidDeviceService,
|
AndroidDeviceService,
|
||||||
|
PDD_PACKAGE_NAME,
|
||||||
)
|
)
|
||||||
from .admin_gateway import (
|
from .admin_gateway import (
|
||||||
AdminGatewayError,
|
AdminGatewayError,
|
||||||
@@ -149,6 +150,48 @@ class AndroidDeviceSearchWorker(QObject):
|
|||||||
self.completed.emit()
|
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):
|
class AndroidWifiConversionWorker(QObject):
|
||||||
"""在后台引导一台 USB 设备切换到 Wi-Fi ADB。"""
|
"""在后台引导一台 USB 设备切换到 Wi-Fi ADB。"""
|
||||||
|
|
||||||
@@ -244,7 +287,6 @@ class SettingsPageEventBinder(QObject):
|
|||||||
"""绑定设备管理控件,并向应用层发出稳定事件。"""
|
"""绑定设备管理控件,并向应用层发出稳定事件。"""
|
||||||
|
|
||||||
searchRequested = pyqtSignal()
|
searchRequested = pyqtSignal()
|
||||||
connectRequested = pyqtSignal(str)
|
|
||||||
convertWifiRequested = pyqtSignal(str)
|
convertWifiRequested = pyqtSignal(str)
|
||||||
currentDeviceSaveRequested = pyqtSignal(str, str)
|
currentDeviceSaveRequested = pyqtSignal(str, str)
|
||||||
saveRequested = pyqtSignal(str, str)
|
saveRequested = pyqtSignal(str, str)
|
||||||
@@ -269,6 +311,14 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._search_is_startup_restore = False
|
self._search_is_startup_restore = False
|
||||||
self._search_thread: Optional[QThread] = None
|
self._search_thread: Optional[QThread] = None
|
||||||
self._search_worker: Optional[AndroidDeviceSearchWorker] = 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_busy = False
|
||||||
self._wifi_conversion_thread: Optional[QThread] = None
|
self._wifi_conversion_thread: Optional[QThread] = None
|
||||||
self._wifi_conversion_worker: Optional[
|
self._wifi_conversion_worker: Optional[
|
||||||
@@ -309,15 +359,15 @@ class SettingsPageEventBinder(QObject):
|
|||||||
)
|
)
|
||||||
page.searchButton.clicked.connect(self._request_search)
|
page.searchButton.clicked.connect(self._request_search)
|
||||||
self.searchRequested.connect(self._start_search)
|
self.searchRequested.connect(self._start_search)
|
||||||
page.connectButton.clicked.connect(self._request_connect)
|
|
||||||
page.convertWifiButton.clicked.connect(self._request_convert_wifi)
|
page.convertWifiButton.clicked.connect(self._request_convert_wifi)
|
||||||
self.convertWifiRequested.connect(self._start_convert_wifi)
|
self.convertWifiRequested.connect(self._start_convert_wifi)
|
||||||
page.saveButton.clicked.connect(self._request_save)
|
page.saveButton.clicked.connect(self._request_save)
|
||||||
self.saveRequested.connect(self._start_save_android_device)
|
self.saveRequested.connect(self._start_save_android_device)
|
||||||
page.deleteButton.clicked.connect(self._request_delete)
|
page.deleteButton.clicked.connect(self._request_delete)
|
||||||
self.deleteRequested.connect(self._start_delete_android_device)
|
self.deleteRequested.connect(self._start_delete_android_device)
|
||||||
page.addressInput.textChanged.connect(self._sync_button_state)
|
page.deviceTableModel.checkedDeviceChanged.connect(
|
||||||
page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state)
|
self._on_checked_device_changed
|
||||||
|
)
|
||||||
page.destroyed.connect(self.shutdown)
|
page.destroyed.connect(self.shutdown)
|
||||||
application = QCoreApplication.instance()
|
application = QCoreApplication.instance()
|
||||||
if application is not None:
|
if application is not None:
|
||||||
@@ -469,8 +519,35 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._search_worker = worker
|
self._search_worker = worker
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot(str)
|
||||||
def _request_connect(self) -> None:
|
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 (
|
if (
|
||||||
self._busy
|
self._busy
|
||||||
or self._current_device_busy
|
or self._current_device_busy
|
||||||
@@ -478,16 +555,115 @@ class SettingsPageEventBinder(QObject):
|
|||||||
or self._wifi_conversion_busy
|
or self._wifi_conversion_busy
|
||||||
or self._android_setting_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
|
return
|
||||||
|
|
||||||
address = self._page.addressInput.text().strip()
|
self._pending_pdd_check_serial = ""
|
||||||
if not address:
|
self._pdd_check_busy = True
|
||||||
self._page.deviceStatusLabel.setText(
|
self._pdd_check_serial = serial
|
||||||
"连接失败:请先填写无线调试地址,例如 192.168.0.173:5555"
|
self._page.set_pdd_app_status("正在检测…")
|
||||||
|
self._sync_button_state()
|
||||||
|
|
||||||
|
thread = QThread(self)
|
||||||
|
worker = PddPackageCheckWorker(
|
||||||
|
self._android_device_service,
|
||||||
|
serial,
|
||||||
)
|
)
|
||||||
self._page.addressInput.setFocus()
|
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
|
return
|
||||||
self.connectRequested.emit(address)
|
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()
|
@pyqtSlot()
|
||||||
def _request_convert_wifi(self) -> None:
|
def _request_convert_wifi(self) -> None:
|
||||||
@@ -567,7 +743,11 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._page.deviceTable.setFocus()
|
self._page.deviceTable.setFocus()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.saveRequested.emit("", serial)
|
self._pending_android_save_serial = serial
|
||||||
|
self._page.deviceStatusLabel.setText(
|
||||||
|
"正在确认所选设备是否安装 PDD…"
|
||||||
|
)
|
||||||
|
self._queue_pdd_package_check(serial)
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def _start_save_android_device(
|
def _start_save_android_device(
|
||||||
@@ -640,7 +820,6 @@ class SettingsPageEventBinder(QObject):
|
|||||||
def _sync_button_state(self, *_args) -> None:
|
def _sync_button_state(self, *_args) -> None:
|
||||||
serial = self._page.deviceTableModel.checked_serial
|
serial = self._page.deviceTableModel.checked_serial
|
||||||
selected_device = self._page.deviceTableModel.device_for_serial(serial)
|
selected_device = self._page.deviceTableModel.device_for_serial(serial)
|
||||||
has_address = bool(self._page.addressInput.text().strip())
|
|
||||||
self._page.currentDeviceSaveButton.setEnabled(
|
self._page.currentDeviceSaveButton.setEnabled(
|
||||||
not self._busy
|
not self._busy
|
||||||
and not self._current_device_busy
|
and not self._current_device_busy
|
||||||
@@ -657,11 +836,10 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._page.searchButton.setEnabled(
|
self._page.searchButton.setEnabled(
|
||||||
device_commands_enabled and not self._current_device_busy
|
device_commands_enabled and not self._current_device_busy
|
||||||
)
|
)
|
||||||
self._page.connectButton.setEnabled(
|
|
||||||
device_commands_enabled and has_address
|
|
||||||
)
|
|
||||||
self._page.saveButton.setEnabled(
|
self._page.saveButton.setEnabled(
|
||||||
device_commands_enabled and bool(serial)
|
device_commands_enabled
|
||||||
|
and not self._pdd_check_busy
|
||||||
|
and bool(serial)
|
||||||
)
|
)
|
||||||
self._page.convertWifiButton.setEnabled(
|
self._page.convertWifiButton.setEnabled(
|
||||||
device_commands_enabled
|
device_commands_enabled
|
||||||
@@ -775,6 +953,12 @@ class SettingsPageEventBinder(QObject):
|
|||||||
"未找到 Android 设备,请检查 USB 调试或无线连接后重试"
|
"未找到 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)
|
@pyqtSlot(str)
|
||||||
def _on_search_failed(self, message: str) -> None:
|
def _on_search_failed(self, message: str) -> None:
|
||||||
if self._closing:
|
if self._closing:
|
||||||
@@ -803,6 +987,9 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._search_is_startup_restore = False
|
self._search_is_startup_restore = False
|
||||||
if not self._closing:
|
if not self._closing:
|
||||||
self._sync_button_state()
|
self._sync_button_state()
|
||||||
|
pending_serial = self._pending_pdd_check_serial
|
||||||
|
if pending_serial:
|
||||||
|
self._queue_pdd_package_check(pending_serial)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def _on_wifi_conversion_progress(self, message: str) -> None:
|
def _on_wifi_conversion_progress(self, message: str) -> None:
|
||||||
@@ -846,6 +1033,9 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._wifi_conversion_busy = False
|
self._wifi_conversion_busy = False
|
||||||
if not self._closing:
|
if not self._closing:
|
||||||
self._sync_button_state()
|
self._sync_button_state()
|
||||||
|
pending_serial = self._pending_pdd_check_serial
|
||||||
|
if pending_serial:
|
||||||
|
self._queue_pdd_package_check(pending_serial)
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def _on_android_device_setting_succeeded(
|
def _on_android_device_setting_succeeded(
|
||||||
@@ -952,25 +1142,60 @@ class SettingsPageEventBinder(QObject):
|
|||||||
search_worker = self._search_worker
|
search_worker = self._search_worker
|
||||||
search_thread = self._search_thread
|
search_thread = self._search_thread
|
||||||
if search_worker is not None:
|
if search_worker is not None:
|
||||||
|
try:
|
||||||
search_worker.cancel()
|
search_worker.cancel()
|
||||||
for signal, slot in (
|
signal_slots = (
|
||||||
(search_worker.succeeded, self._on_search_succeeded),
|
(search_worker.succeeded, self._on_search_succeeded),
|
||||||
(search_worker.failed, self._on_search_failed),
|
(search_worker.failed, self._on_search_failed),
|
||||||
):
|
)
|
||||||
|
except RuntimeError:
|
||||||
|
# Worker 已调用 deleteLater,但 finished 清理槽还在消息队列中。
|
||||||
|
signal_slots = ()
|
||||||
|
for signal, slot in signal_slots:
|
||||||
try:
|
try:
|
||||||
signal.disconnect(slot)
|
signal.disconnect(slot)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if search_thread is not None and search_thread.isRunning():
|
if search_thread is not None and search_thread.isRunning():
|
||||||
search_thread.quit()
|
search_thread.quit()
|
||||||
search_thread.wait(6000)
|
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_worker = self._wifi_conversion_worker
|
||||||
conversion_thread = self._wifi_conversion_thread
|
conversion_thread = self._wifi_conversion_thread
|
||||||
if conversion_worker is not None:
|
if conversion_worker is not None:
|
||||||
|
try:
|
||||||
conversion_worker.cancel()
|
conversion_worker.cancel()
|
||||||
for signal, slot in (
|
signal_slots = (
|
||||||
(
|
(
|
||||||
conversion_worker.progressChanged,
|
conversion_worker.progressChanged,
|
||||||
self._on_wifi_conversion_progress,
|
self._on_wifi_conversion_progress,
|
||||||
@@ -983,10 +1208,13 @@ class SettingsPageEventBinder(QObject):
|
|||||||
conversion_worker.failed,
|
conversion_worker.failed,
|
||||||
self._on_wifi_conversion_failed,
|
self._on_wifi_conversion_failed,
|
||||||
),
|
),
|
||||||
):
|
)
|
||||||
|
except RuntimeError:
|
||||||
|
signal_slots = ()
|
||||||
|
for signal, slot in signal_slots:
|
||||||
try:
|
try:
|
||||||
signal.disconnect(slot)
|
signal.disconnect(slot)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if conversion_thread is not None and conversion_thread.isRunning():
|
if conversion_thread is not None and conversion_thread.isRunning():
|
||||||
@@ -996,8 +1224,9 @@ class SettingsPageEventBinder(QObject):
|
|||||||
setting_worker = self._android_setting_worker
|
setting_worker = self._android_setting_worker
|
||||||
setting_thread = self._android_setting_thread
|
setting_thread = self._android_setting_thread
|
||||||
if setting_worker is not None:
|
if setting_worker is not None:
|
||||||
|
try:
|
||||||
setting_worker.cancel()
|
setting_worker.cancel()
|
||||||
for signal, slot in (
|
signal_slots = (
|
||||||
(
|
(
|
||||||
setting_worker.succeeded,
|
setting_worker.succeeded,
|
||||||
self._on_android_device_setting_succeeded,
|
self._on_android_device_setting_succeeded,
|
||||||
@@ -1006,10 +1235,13 @@ class SettingsPageEventBinder(QObject):
|
|||||||
setting_worker.failed,
|
setting_worker.failed,
|
||||||
self._on_android_device_setting_failed,
|
self._on_android_device_setting_failed,
|
||||||
),
|
),
|
||||||
):
|
)
|
||||||
|
except RuntimeError:
|
||||||
|
signal_slots = ()
|
||||||
|
for signal, slot in signal_slots:
|
||||||
try:
|
try:
|
||||||
signal.disconnect(slot)
|
signal.disconnect(slot)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if setting_thread is not None and setting_thread.isRunning():
|
if setting_thread is not None and setting_thread.isRunning():
|
||||||
|
|||||||
@@ -202,6 +202,55 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
with self.assertRaisesRegex(AndroidDeviceSearchError, "didn't ACK"):
|
with self.assertRaisesRegex(AndroidDeviceSearchError, "didn't ACK"):
|
||||||
AndroidDeviceService(runner).search()
|
AndroidDeviceService(runner).search()
|
||||||
|
|
||||||
|
def test_package_is_installed_when_pm_returns_apk_path(self):
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
def runner(command, _timeout):
|
||||||
|
commands.append(list(command))
|
||||||
|
return completed(
|
||||||
|
command,
|
||||||
|
"package:/data/app/com.xunmeng.pinduoduo/base.apk\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
installed = AndroidDeviceService(runner).is_package_installed(
|
||||||
|
"USB-001"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(installed)
|
||||||
|
self.assertEqual(
|
||||||
|
commands,
|
||||||
|
[[
|
||||||
|
"adb",
|
||||||
|
"-s",
|
||||||
|
"USB-001",
|
||||||
|
"shell",
|
||||||
|
"pm",
|
||||||
|
"path",
|
||||||
|
"com.xunmeng.pinduoduo",
|
||||||
|
]],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_package_is_not_installed_when_pm_returns_no_path(self):
|
||||||
|
def runner(command, _timeout):
|
||||||
|
return completed(command, "")
|
||||||
|
|
||||||
|
installed = AndroidDeviceService(runner).is_package_installed(
|
||||||
|
"USB-001"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(installed)
|
||||||
|
|
||||||
|
def test_package_check_reports_adb_failure(self):
|
||||||
|
def runner(command, _timeout):
|
||||||
|
return completed(
|
||||||
|
command,
|
||||||
|
stderr="device offline",
|
||||||
|
returncode=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(AndroidDeviceSearchError, "device offline"):
|
||||||
|
AndroidDeviceService(runner).is_package_installed("USB-001")
|
||||||
|
|
||||||
def test_convert_usb_to_wifi_connects_while_usb_is_attached(self):
|
def test_convert_usb_to_wifi_connects_while_usb_is_attached(self):
|
||||||
commands = []
|
commands = []
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class SequenceAndroidDeviceService:
|
|||||||
self.restore_serials.append(serial)
|
self.restore_serials.append(serial)
|
||||||
return self.search(is_cancelled)
|
return self.search(is_cancelled)
|
||||||
|
|
||||||
|
def is_package_installed(self, _serial, _package_name):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class SlowAndroidDeviceService:
|
class SlowAndroidDeviceService:
|
||||||
"""模拟较慢的 ADB 搜索。"""
|
"""模拟较慢的 ADB 搜索。"""
|
||||||
@@ -68,6 +71,9 @@ class SlowAndroidDeviceService:
|
|||||||
self.restore_serial = serial
|
self.restore_serial = serial
|
||||||
return self.search(is_cancelled)
|
return self.search(is_cancelled)
|
||||||
|
|
||||||
|
def is_package_installed(self, _serial, _package_name):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ControlledWifiConversionService:
|
class ControlledWifiConversionService:
|
||||||
"""模拟 USB 转 Wi-Fi 的进度、成功、失败和延迟。"""
|
"""模拟 USB 转 Wi-Fi 的进度、成功、失败和延迟。"""
|
||||||
@@ -76,10 +82,18 @@ class ControlledWifiConversionService:
|
|||||||
self.delay = delay
|
self.delay = delay
|
||||||
self.error = error
|
self.error = error
|
||||||
self.call_count = 0
|
self.call_count = 0
|
||||||
|
self.search_count = 0
|
||||||
|
|
||||||
def search(self, _is_cancelled=None):
|
def search(self, _is_cancelled=None):
|
||||||
|
self.search_count += 1
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def restore_saved_device(self, _serial, is_cancelled=None):
|
||||||
|
return self.search(is_cancelled)
|
||||||
|
|
||||||
|
def is_package_installed(self, _serial, _package_name):
|
||||||
|
return True
|
||||||
|
|
||||||
def convert_usb_to_wifi(
|
def convert_usb_to_wifi(
|
||||||
self,
|
self,
|
||||||
usb_serial,
|
usb_serial,
|
||||||
@@ -106,6 +120,34 @@ class ControlledWifiConversionService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ControlledPackageCheckService:
|
||||||
|
"""控制 PDD 包检测结果,其他 ADB 行为返回空列表。"""
|
||||||
|
|
||||||
|
def __init__(self, installed=True, delay=0.0, error=None):
|
||||||
|
self.installed = installed
|
||||||
|
self.delay = delay
|
||||||
|
self.error = error
|
||||||
|
self.checked_serials = []
|
||||||
|
self.search_count = 0
|
||||||
|
|
||||||
|
def search(self, _is_cancelled=None):
|
||||||
|
self.search_count += 1
|
||||||
|
return []
|
||||||
|
|
||||||
|
def restore_saved_device(self, _serial, _is_cancelled=None):
|
||||||
|
return self.search(_is_cancelled)
|
||||||
|
|
||||||
|
def is_package_installed(self, serial, _package_name):
|
||||||
|
self.checked_serials.append(serial)
|
||||||
|
if self.delay:
|
||||||
|
time.sleep(self.delay)
|
||||||
|
if self.error is not None:
|
||||||
|
raise self.error
|
||||||
|
if isinstance(self.installed, dict):
|
||||||
|
return self.installed.get(serial, False)
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
|
||||||
class ControlledSettingsRepository(SettingsRepository):
|
class ControlledSettingsRepository(SettingsRepository):
|
||||||
"""控制 Android 设备设置写入速度和失败,其他设置保持正常。"""
|
"""控制 Android 设备设置写入速度和失败,其他设置保持正常。"""
|
||||||
|
|
||||||
@@ -399,19 +441,24 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
def test_selected_android_device_is_saved_to_sqlite(self):
|
def test_selected_android_device_is_saved_to_sqlite(self):
|
||||||
|
service = ControlledPackageCheckService()
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=self.repository,
|
settings_repository=self.repository,
|
||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
page.set_android_devices(
|
page.set_android_devices(
|
||||||
[AndroidDeviceRow("USB-001", "USB", "Pixel 8", "14")]
|
[AndroidDeviceRow("USB-001", "USB", "Pixel 8", "14")]
|
||||||
)
|
)
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
page.saveButton.click()
|
page.saveButton.click()
|
||||||
self.assertFalse(page.saveButton.isEnabled())
|
self.assertFalse(page.saveButton.isEnabled())
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
lambda: page.eventBinder._android_setting_thread is None
|
lambda: self.repository.get(SELECTED_ANDROID_SERIAL_KEY)
|
||||||
|
== "USB-001"
|
||||||
|
and page.eventBinder._android_setting_thread is None
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -670,13 +717,89 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
page.eventBinder.shutdown()
|
page.eventBinder.shutdown()
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
def test_selected_android_device_save_failure_keeps_old_configuration(self):
|
def test_selected_device_shows_installed_pdd_package(self):
|
||||||
repository = ControlledSettingsRepository(self.db_path)
|
service = ControlledPackageCheckService(installed=True)
|
||||||
repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-OLD")
|
|
||||||
repository.fail_selected_write = True
|
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=repository,
|
settings_repository=self.repository,
|
||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
|
)
|
||||||
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
|
|
||||||
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
|
self.assertEqual(service.checked_serials, ["USB-001"])
|
||||||
|
self.assertEqual(
|
||||||
|
page.pddAppStatusLabel.text(),
|
||||||
|
"com.xunmeng.pinduoduo(已安装)",
|
||||||
|
)
|
||||||
|
self.assertFalse(hasattr(page, "addressInput"))
|
||||||
|
self.assertFalse(hasattr(page, "connectButton"))
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_save_rechecks_pdd_and_blocks_device_without_app(self):
|
||||||
|
service = ControlledPackageCheckService(installed=False)
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
|
)
|
||||||
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
|
page.saveButton.click()
|
||||||
|
self._wait_until(
|
||||||
|
lambda: len(service.checked_serials) == 2
|
||||||
|
and page.eventBinder._pdd_check_thread is None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(
|
||||||
|
self.repository.get(SELECTED_ANDROID_SERIAL_KEY)
|
||||||
|
)
|
||||||
|
self.assertIn("未安装", page.pddAppStatusLabel.text())
|
||||||
|
self.assertIn("保存失败", page.deviceStatusLabel.text())
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_save_blocks_when_pdd_check_fails(self):
|
||||||
|
service = ControlledPackageCheckService(
|
||||||
|
error=AndroidDeviceSearchError("device offline")
|
||||||
|
)
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
|
)
|
||||||
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
|
page.saveButton.click()
|
||||||
|
self._wait_until(
|
||||||
|
lambda: len(service.checked_serials) == 2
|
||||||
|
and page.eventBinder._pdd_check_thread is None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(
|
||||||
|
self.repository.get(SELECTED_ANDROID_SERIAL_KEY)
|
||||||
|
)
|
||||||
|
self.assertIn("检测失败", page.pddAppStatusLabel.text())
|
||||||
|
self.assertIn("原配置未更改", page.deviceStatusLabel.text())
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_device_change_ignores_stale_pdd_check_result(self):
|
||||||
|
service = ControlledPackageCheckService(
|
||||||
|
installed={"USB-OLD": True, "USB-NEW": False},
|
||||||
|
delay=0.05,
|
||||||
|
)
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
page.set_android_devices(
|
page.set_android_devices(
|
||||||
[
|
[
|
||||||
@@ -684,11 +807,45 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
AndroidDeviceRow("USB-NEW", "USB"),
|
AndroidDeviceRow("USB-NEW", "USB"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
page.deviceTableModel.set_checked_serial("USB-OLD")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_busy)
|
||||||
page.deviceTableModel.set_checked_serial("USB-NEW")
|
page.deviceTableModel.set_checked_serial("USB-NEW")
|
||||||
|
self._wait_until(
|
||||||
|
lambda: service.checked_serials == ["USB-OLD", "USB-NEW"]
|
||||||
|
and page.eventBinder._pdd_check_thread is None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(page.deviceTableModel.checked_serial, "USB-NEW")
|
||||||
|
self.assertIn("未安装", page.pddAppStatusLabel.text())
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_selected_android_device_save_failure_keeps_old_configuration(self):
|
||||||
|
repository = ControlledSettingsRepository(self.db_path)
|
||||||
|
repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-OLD")
|
||||||
|
repository.fail_selected_write = True
|
||||||
|
service = ControlledPackageCheckService()
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=repository,
|
||||||
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
|
)
|
||||||
|
self._wait_until(lambda: service.search_count == 1)
|
||||||
|
self._wait_until(lambda: page.eventBinder._search_thread is None)
|
||||||
|
page.set_android_devices(
|
||||||
|
[
|
||||||
|
AndroidDeviceRow("USB-OLD", "USB"),
|
||||||
|
AndroidDeviceRow("USB-NEW", "USB"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
page.deviceTableModel.set_checked_serial("USB-NEW")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
page.saveButton.click()
|
page.saveButton.click()
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
lambda: page.eventBinder._android_setting_thread is None
|
lambda: "原设备配置未更改" in page.deviceStatusLabel.text()
|
||||||
|
and page.eventBinder._android_setting_thread is None
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -727,12 +884,15 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_slow_android_device_save_does_not_block_or_start_twice(self):
|
def test_slow_android_device_save_does_not_block_or_start_twice(self):
|
||||||
repository = ControlledSettingsRepository(self.db_path, delay=0.08)
|
repository = ControlledSettingsRepository(self.db_path, delay=0.08)
|
||||||
|
service = ControlledPackageCheckService()
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=repository,
|
settings_repository=repository,
|
||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
timer_fired = []
|
timer_fired = []
|
||||||
QTimer.singleShot(10, lambda: timer_fired.append(True))
|
QTimer.singleShot(10, lambda: timer_fired.append(True))
|
||||||
|
|
||||||
@@ -740,7 +900,8 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
page.eventBinder._request_save()
|
page.eventBinder._request_save()
|
||||||
self._wait_until(lambda: bool(timer_fired), timeout_ms=500)
|
self._wait_until(lambda: bool(timer_fired), timeout_ms=500)
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
lambda: page.eventBinder._android_setting_thread is None
|
lambda: repository.selected_write_count == 1
|
||||||
|
and page.eventBinder._android_setting_thread is None
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(timer_fired)
|
self.assertTrue(timer_fired)
|
||||||
@@ -750,12 +911,15 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_shutdown_ignores_late_android_device_save_result(self):
|
def test_shutdown_ignores_late_android_device_save_result(self):
|
||||||
repository = ControlledSettingsRepository(self.db_path, delay=0.08)
|
repository = ControlledSettingsRepository(self.db_path, delay=0.08)
|
||||||
|
service = ControlledPackageCheckService()
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=repository,
|
settings_repository=repository,
|
||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
page.saveButton.click()
|
page.saveButton.click()
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
@@ -769,21 +933,25 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
def test_convert_wifi_button_requires_connected_usb_device(self):
|
def test_convert_wifi_button_requires_connected_usb_device(self):
|
||||||
|
service = ControlledPackageCheckService()
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=self.repository,
|
settings_repository=self.repository,
|
||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(page.convertWifiButton.isEnabled())
|
self.assertFalse(page.convertWifiButton.isEnabled())
|
||||||
|
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
self.assertTrue(page.convertWifiButton.isEnabled())
|
self.assertTrue(page.convertWifiButton.isEnabled())
|
||||||
|
|
||||||
page.set_android_devices(
|
page.set_android_devices(
|
||||||
[AndroidDeviceRow("192.168.0.173:5555", "Wi-Fi")]
|
[AndroidDeviceRow("192.168.0.173:5555", "Wi-Fi")]
|
||||||
)
|
)
|
||||||
page.deviceTableModel.set_checked_serial("192.168.0.173:5555")
|
page.deviceTableModel.set_checked_serial("192.168.0.173:5555")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
self.assertFalse(page.convertWifiButton.isEnabled())
|
self.assertFalse(page.convertWifiButton.isEnabled())
|
||||||
|
|
||||||
page.set_android_devices(
|
page.set_android_devices(
|
||||||
@@ -802,13 +970,17 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
android_device_service=service,
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
|
self._wait_until(lambda: service.search_count == 1)
|
||||||
|
self._wait_until(lambda: page.eventBinder._search_thread is None)
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
page.convertWifiButton.click()
|
page.convertWifiButton.click()
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
lambda: page.eventBinder._wifi_conversion_thread is None
|
lambda: page.eventBinder._wifi_conversion_thread is None
|
||||||
)
|
)
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
self.assertEqual(service.last_usb_serial, "USB-001")
|
self.assertEqual(service.last_usb_serial, "USB-001")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -844,8 +1016,11 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
admin_gateway=MockAdminGateway(),
|
admin_gateway=MockAdminGateway(),
|
||||||
android_device_service=service,
|
android_device_service=service,
|
||||||
)
|
)
|
||||||
|
self._wait_until(lambda: service.search_count == 1)
|
||||||
|
self._wait_until(lambda: page.eventBinder._search_thread is None)
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
page.convertWifiButton.click()
|
page.convertWifiButton.click()
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
@@ -875,6 +1050,7 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
timer_fired = []
|
timer_fired = []
|
||||||
QTimer.singleShot(10, lambda: timer_fired.append(True))
|
QTimer.singleShot(10, lambda: timer_fired.append(True))
|
||||||
|
|
||||||
@@ -899,6 +1075,7 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||||
page.deviceTableModel.set_checked_serial("USB-001")
|
page.deviceTableModel.set_checked_serial("USB-001")
|
||||||
|
self._wait_until(lambda: page.eventBinder._pdd_check_thread is None)
|
||||||
|
|
||||||
page.convertWifiButton.click()
|
page.convertWifiButton.click()
|
||||||
self._wait_until(
|
self._wait_until(
|
||||||
|
|||||||
Reference in New Issue
Block a user