feat: 检测 Android 设备 PDD 安装状态 (#29)
This commit is contained in:
+285
-53
@@ -19,6 +19,7 @@ from .android_device_service import (
|
||||
AndroidDeviceConversionCancelled,
|
||||
AndroidDeviceSearchError,
|
||||
AndroidDeviceService,
|
||||
PDD_PACKAGE_NAME,
|
||||
)
|
||||
from .admin_gateway import (
|
||||
AdminGatewayError,
|
||||
@@ -149,6 +150,48 @@ class AndroidDeviceSearchWorker(QObject):
|
||||
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。"""
|
||||
|
||||
@@ -244,7 +287,6 @@ class SettingsPageEventBinder(QObject):
|
||||
"""绑定设备管理控件,并向应用层发出稳定事件。"""
|
||||
|
||||
searchRequested = pyqtSignal()
|
||||
connectRequested = pyqtSignal(str)
|
||||
convertWifiRequested = pyqtSignal(str)
|
||||
currentDeviceSaveRequested = pyqtSignal(str, str)
|
||||
saveRequested = pyqtSignal(str, str)
|
||||
@@ -269,6 +311,14 @@ class SettingsPageEventBinder(QObject):
|
||||
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[
|
||||
@@ -309,15 +359,15 @@ class SettingsPageEventBinder(QObject):
|
||||
)
|
||||
page.searchButton.clicked.connect(self._request_search)
|
||||
self.searchRequested.connect(self._start_search)
|
||||
page.connectButton.clicked.connect(self._request_connect)
|
||||
page.convertWifiButton.clicked.connect(self._request_convert_wifi)
|
||||
self.convertWifiRequested.connect(self._start_convert_wifi)
|
||||
page.saveButton.clicked.connect(self._request_save)
|
||||
self.saveRequested.connect(self._start_save_android_device)
|
||||
page.deleteButton.clicked.connect(self._request_delete)
|
||||
self.deleteRequested.connect(self._start_delete_android_device)
|
||||
page.addressInput.textChanged.connect(self._sync_button_state)
|
||||
page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state)
|
||||
page.deviceTableModel.checkedDeviceChanged.connect(
|
||||
self._on_checked_device_changed
|
||||
)
|
||||
page.destroyed.connect(self.shutdown)
|
||||
application = QCoreApplication.instance()
|
||||
if application is not None:
|
||||
@@ -469,8 +519,35 @@ class SettingsPageEventBinder(QObject):
|
||||
self._search_worker = worker
|
||||
thread.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def _request_connect(self) -> None:
|
||||
@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
|
||||
@@ -478,16 +555,115 @@ class SettingsPageEventBinder(QObject):
|
||||
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
|
||||
|
||||
address = self._page.addressInput.text().strip()
|
||||
if not address:
|
||||
self._page.deviceStatusLabel.setText(
|
||||
"连接失败:请先填写无线调试地址,例如 192.168.0.173:5555"
|
||||
)
|
||||
self._page.addressInput.setFocus()
|
||||
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
|
||||
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()
|
||||
def _request_convert_wifi(self) -> None:
|
||||
@@ -567,7 +743,11 @@ class SettingsPageEventBinder(QObject):
|
||||
self._page.deviceTable.setFocus()
|
||||
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)
|
||||
def _start_save_android_device(
|
||||
@@ -640,7 +820,6 @@ class SettingsPageEventBinder(QObject):
|
||||
def _sync_button_state(self, *_args) -> None:
|
||||
serial = self._page.deviceTableModel.checked_serial
|
||||
selected_device = self._page.deviceTableModel.device_for_serial(serial)
|
||||
has_address = bool(self._page.addressInput.text().strip())
|
||||
self._page.currentDeviceSaveButton.setEnabled(
|
||||
not self._busy
|
||||
and not self._current_device_busy
|
||||
@@ -657,11 +836,10 @@ class SettingsPageEventBinder(QObject):
|
||||
self._page.searchButton.setEnabled(
|
||||
device_commands_enabled and not self._current_device_busy
|
||||
)
|
||||
self._page.connectButton.setEnabled(
|
||||
device_commands_enabled and has_address
|
||||
)
|
||||
self._page.saveButton.setEnabled(
|
||||
device_commands_enabled and bool(serial)
|
||||
device_commands_enabled
|
||||
and not self._pdd_check_busy
|
||||
and bool(serial)
|
||||
)
|
||||
self._page.convertWifiButton.setEnabled(
|
||||
device_commands_enabled
|
||||
@@ -775,6 +953,12 @@ class SettingsPageEventBinder(QObject):
|
||||
"未找到 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:
|
||||
@@ -803,6 +987,9 @@ class SettingsPageEventBinder(QObject):
|
||||
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:
|
||||
@@ -846,6 +1033,9 @@ class SettingsPageEventBinder(QObject):
|
||||
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(
|
||||
@@ -952,41 +1142,79 @@ class SettingsPageEventBinder(QObject):
|
||||
search_worker = self._search_worker
|
||||
search_thread = self._search_thread
|
||||
if search_worker is not None:
|
||||
search_worker.cancel()
|
||||
for signal, slot in (
|
||||
(search_worker.succeeded, self._on_search_succeeded),
|
||||
(search_worker.failed, self._on_search_failed),
|
||||
):
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
conversion_worker.cancel()
|
||||
for signal, slot in (
|
||||
(
|
||||
conversion_worker.progressChanged,
|
||||
self._on_wifi_conversion_progress,
|
||||
),
|
||||
(
|
||||
conversion_worker.succeeded,
|
||||
self._on_wifi_conversion_succeeded,
|
||||
),
|
||||
(
|
||||
conversion_worker.failed,
|
||||
self._on_wifi_conversion_failed,
|
||||
),
|
||||
):
|
||||
try:
|
||||
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:
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
|
||||
if conversion_thread is not None and conversion_thread.isRunning():
|
||||
@@ -996,20 +1224,24 @@ class SettingsPageEventBinder(QObject):
|
||||
setting_worker = self._android_setting_worker
|
||||
setting_thread = self._android_setting_thread
|
||||
if setting_worker is not None:
|
||||
setting_worker.cancel()
|
||||
for signal, slot in (
|
||||
(
|
||||
setting_worker.succeeded,
|
||||
self._on_android_device_setting_succeeded,
|
||||
),
|
||||
(
|
||||
setting_worker.failed,
|
||||
self._on_android_device_setting_failed,
|
||||
),
|
||||
):
|
||||
try:
|
||||
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:
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
|
||||
if setting_thread is not None and setting_thread.isRunning():
|
||||
|
||||
Reference in New Issue
Block a user