From 27a09e524c67d24c21fe2981f6103736b347fe13 Mon Sep 17 00:00:00 2001 From: chengma Date: Fri, 7 Aug 2026 14:13:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=9D=E5=AD=98=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84=20Android=20=E8=AE=BE=E5=A4=87=20(#?= =?UTF-8?q?22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/selected_android_device_service.py | 50 ++++ client/src/settings_ui_event.py | 254 ++++++++++++++++-- .../test_selected_android_device_service.py | 60 +++++ client/test/test_settings_ui_event.py | 211 ++++++++++++++- 4 files changed, 558 insertions(+), 17 deletions(-) create mode 100644 client/src/selected_android_device_service.py create mode 100644 client/test/test_selected_android_device_service.py diff --git a/client/src/selected_android_device_service.py b/client/src/selected_android_device_service.py new file mode 100644 index 0000000..7ead1ca --- /dev/null +++ b/client/src/selected_android_device_service.py @@ -0,0 +1,50 @@ +"""当前使用 Android 设备的本地设置服务。""" + +from .settings_repository import SettingsRepository + + +SELECTED_ANDROID_SERIAL_KEY = "android.selected_serial" +MAX_ANDROID_SERIAL_LENGTH = 255 + + +class SelectedAndroidDeviceService: + """只保存自动化应使用的设备号,不缓存 ADB 搜索结果。""" + + def __init__(self, repository: SettingsRepository): + self._repository = repository + + def load(self) -> str: + """读取已保存设备号;缺失或损坏时返回空字符串。""" + + value = self._repository.get(SELECTED_ANDROID_SERIAL_KEY, "") + if not isinstance(value, str): + return "" + return value.strip() + + def save(self, serial: str) -> str: + """校验并保存设备号,返回规范化后的值。""" + + normalized = self._normalize_serial(serial) + self._repository.set(SELECTED_ANDROID_SERIAL_KEY, normalized) + return normalized + + def delete(self) -> bool: + """清除当前设备配置;配置不存在时也视为安全完成。""" + + return self._repository.delete(SELECTED_ANDROID_SERIAL_KEY) + + @staticmethod + def _normalize_serial(serial: object) -> str: + if not isinstance(serial, str): + raise ValueError("Android 设备号必须是文本") + + normalized = serial.strip() + if not normalized: + raise ValueError("Android 设备号不能为空") + if len(normalized) > MAX_ANDROID_SERIAL_LENGTH: + raise ValueError( + f"Android 设备号最多 {MAX_ANDROID_SERIAL_LENGTH} 个字符" + ) + if any(character.isspace() for character in normalized): + raise ValueError("Android 设备号不能包含空白字符") + return normalized diff --git a/client/src/settings_ui_event.py b/client/src/settings_ui_event.py index 9e5a02c..f364588 100644 --- a/client/src/settings_ui_event.py +++ b/client/src/settings_ui_event.py @@ -1,7 +1,7 @@ """设置页事件、ADB 搜索、当前 Client 本地保存和后台登记。 -本文件不直接写 SQL。ADB、SQLite 保存和 Admin HTTP 请求由 Worker 在线程中 -执行,后台结果只通过信号返回主线程更新页面。 +本文件不直接写 SQL。ADB 搜索、SQLite 写入和 Admin HTTP 请求由 Worker 在线程中 +执行;页面启动时只同步读取少量索引设置,后台结果通过信号返回主线程更新页面。 """ from typing import Optional @@ -24,6 +24,7 @@ from .current_client_service import ( generate_client_device_id, ) from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway +from .selected_android_device_service import SelectedAndroidDeviceService from .settings_repository import SettingsRepository from .settings_ui import AndroidDeviceRow @@ -129,6 +130,54 @@ class AndroidDeviceSearchWorker(QObject): 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): """绑定设备管理控件,并向应用层发出稳定事件。""" @@ -155,12 +204,19 @@ class SettingsPageEventBinder(QObject): self._search_busy = False self._search_thread: Optional[QThread] = None self._search_worker: Optional[AndroidDeviceSearchWorker] = None + self._android_setting_busy = False + self._android_setting_thread: Optional[QThread] = None + self._android_setting_worker: Optional[AndroidDeviceSettingWorker] = None + self._saved_android_serial = "" self._android_device_service = ( android_device_service or AndroidDeviceService() ) repository = settings_repository or SettingsRepository() self._client_service = CurrentClientService(repository) + self._selected_android_device_service = SelectedAndroidDeviceService( + repository + ) self._admin_gateway = admin_gateway self._gateway_error = "" if self._admin_gateway is None: @@ -185,7 +241,9 @@ class SettingsPageEventBinder(QObject): self.searchRequested.connect(self._start_search) page.connectButton.clicked.connect(self._request_connect) page.saveButton.clicked.connect(self._request_save) + self.saveRequested.connect(self._start_save_android_device) page.deleteButton.clicked.connect(self._request_delete) + self.deleteRequested.connect(self._start_delete_android_device) page.addressInput.textChanged.connect(self._sync_button_state) page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state) page.destroyed.connect(self.shutdown) @@ -194,11 +252,17 @@ class SettingsPageEventBinder(QObject): application.aboutToQuit.connect(self.shutdown) self._load_current_client() + self._load_selected_android_device() self._sync_button_state() @pyqtSlot() def _request_save_current_device(self) -> None: - if self._busy or self._current_device_busy or self._search_busy: + if ( + self._busy + or self._current_device_busy + or self._search_busy + or self._android_setting_busy + ): return device_id = self._page.deviceIdInput.text().strip() @@ -210,7 +274,12 @@ class SettingsPageEventBinder(QObject): def _start_save_current_device( self, _displayed_device_id: str, device_name: str ) -> None: - if self._closing or self._current_device_busy or self._search_busy: + if ( + self._closing + or self._current_device_busy + or self._search_busy + or self._android_setting_busy + ): return try: @@ -250,7 +319,12 @@ class SettingsPageEventBinder(QObject): @pyqtSlot() def _request_search(self) -> None: - if self._busy or self._current_device_busy or self._search_busy: + if ( + self._busy + or self._current_device_busy + or self._search_busy + or self._android_setting_busy + ): return self.searchRequested.emit() @@ -261,6 +335,7 @@ class SettingsPageEventBinder(QObject): or self._busy or self._current_device_busy or self._search_busy + or self._android_setting_busy ): return @@ -300,7 +375,7 @@ class SettingsPageEventBinder(QObject): @pyqtSlot() def _request_save(self) -> None: - if self._busy: + if self._busy or self._search_busy or self._android_setting_busy: return serial = self._page.deviceTableModel.checked_serial @@ -309,21 +384,69 @@ class SettingsPageEventBinder(QObject): self._page.deviceTable.setFocus() return - device_name = self._page.deviceNameInput.text().strip() - self.saveRequested.emit(device_name, serial) + self.saveRequested.emit("", serial) + + @pyqtSlot(str, str) + def _start_save_android_device( + self, _unused_device_name: str, serial: str + ) -> None: + self._start_android_device_setting("save", serial) @pyqtSlot() def _request_delete(self) -> None: - if self._busy: + if self._busy or self._search_busy or self._android_setting_busy: return - serial = self._page.deviceTableModel.checked_serial + serial = self._saved_android_serial if not serial: - self._page.deviceStatusLabel.setText("删除失败:请先勾选一台设备") - self._page.deviceTable.setFocus() + 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._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 @@ -332,8 +455,13 @@ class SettingsPageEventBinder(QObject): not self._busy and not self._current_device_busy and not self._search_busy + and not self._android_setting_busy + ) + device_commands_enabled = ( + not self._busy + and not self._search_busy + and not self._android_setting_busy ) - device_commands_enabled = not self._busy and not self._search_busy self._page.searchButton.setEnabled( device_commands_enabled and not self._current_device_busy ) @@ -344,7 +472,7 @@ class SettingsPageEventBinder(QObject): device_commands_enabled and bool(serial) ) self._page.deleteButton.setEnabled( - device_commands_enabled and bool(serial) + device_commands_enabled and bool(self._saved_android_serial) ) def set_busy(self, busy: bool, message: str = "") -> None: @@ -370,6 +498,23 @@ class SettingsPageEventBinder(QObject): if saved.client_id: self._page.set_current_device_status("本地设备信息已加载") + def _load_selected_android_device(self) -> None: + """页面创建时恢复自动化应使用的 Android 设备号。""" + + try: + serial = self._selected_android_device_service.load() + except Exception as exc: + self._page.deviceStatusLabel.setText( + f"读取已保存 Android 设备失败:{str(exc) or '数据库不可用'}" + ) + return + + self._saved_android_serial = serial + if serial: + self._page.deviceStatusLabel.setText( + f"当前使用设备:{serial}(等待搜索确认连接)" + ) + def _selected_android_device(self) -> Optional[AndroidDeviceInfo]: serial = self._page.deviceTableModel.checked_serial.strip() return AndroidDeviceInfo(serial) if serial else None @@ -390,7 +535,23 @@ class SettingsPageEventBinder(QObject): for device in devices ] self._page.set_android_devices(rows) - if 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 + ) + self._page.deviceStatusLabel.setText( + f"搜索完成:找到 {len(rows)} 台 Android 设备;" + "已自动选择当前使用设备" + ) + elif self._saved_android_serial: + self._page.deviceStatusLabel.setText( + "已保存设备当前未连接:" + f"{self._saved_android_serial};配置已保留" + ) + elif rows: self._page.deviceStatusLabel.setText( f"搜索完成:找到 {len(rows)} 台 Android 设备" ) @@ -404,7 +565,7 @@ class SettingsPageEventBinder(QObject): if self._closing: return self._page.deviceStatusLabel.setText( - f"搜索失败:{message};设备列表未更改" + f"搜索失败:{message};设备列表和已保存配置未更改" ) @pyqtSlot() @@ -415,6 +576,44 @@ class SettingsPageEventBinder(QObject): if not self._closing: self._sync_button_state() + @pyqtSlot(str, str) + def _on_android_device_setting_succeeded( + self, action: str, serial: str + ) -> None: + if self._closing: + return + + if action == "save": + self._saved_android_serial = serial + self._page.deviceStatusLabel.setText( + f"当前使用设备:{serial}(已保存)" + ) + else: + self._saved_android_serial = "" + self._page.deviceTableModel.set_checked_serial("") + self._page.deviceStatusLabel.setText( + "当前使用设备:未选择(本地配置已删除)" + ) + + @pyqtSlot(str, str) + def _on_android_device_setting_failed( + self, action: str, message: str + ) -> None: + if self._closing: + return + action_text = "保存" if action == "save" else "删除" + self._page.deviceStatusLabel.setText( + f"{action_text}失败:{message};原设备配置未更改,可重试" + ) + + @pyqtSlot() + def _on_android_device_setting_finished(self) -> None: + self._android_setting_worker = None + self._android_setting_thread = None + self._android_setting_busy = False + if not self._closing: + self._sync_button_state() + @pyqtSlot(str, str) def _on_local_saved(self, client_id: str, client_name: str) -> None: if self._closing: @@ -495,3 +694,26 @@ class SettingsPageEventBinder(QObject): if search_thread is not None and search_thread.isRunning(): search_thread.quit() search_thread.wait(6000) + + setting_worker = self._android_setting_worker + setting_thread = self._android_setting_thread + if setting_worker is not None: + setting_worker.cancel() + for signal, slot in ( + ( + setting_worker.succeeded, + self._on_android_device_setting_succeeded, + ), + ( + setting_worker.failed, + self._on_android_device_setting_failed, + ), + ): + try: + signal.disconnect(slot) + except TypeError: + pass + + if setting_thread is not None and setting_thread.isRunning(): + setting_thread.quit() + setting_thread.wait(4000) diff --git a/client/test/test_selected_android_device_service.py b/client/test/test_selected_android_device_service.py new file mode 100644 index 0000000..358e0cc --- /dev/null +++ b/client/test/test_selected_android_device_service.py @@ -0,0 +1,60 @@ +"""当前使用 Android 设备设置服务测试。""" + +import tempfile +import unittest +from pathlib import Path + +from src.selected_android_device_service import ( + SELECTED_ANDROID_SERIAL_KEY, + SelectedAndroidDeviceService, +) +from src.settings_repository import SettingsRepository + + +class SelectedAndroidDeviceServiceTest(unittest.TestCase): + def setUp(self): + self.temp_directory = tempfile.TemporaryDirectory() + db_path = Path(self.temp_directory.name) / "client.db" + self.repository = SettingsRepository(db_path) + self.service = SelectedAndroidDeviceService(self.repository) + + def tearDown(self): + self.temp_directory.cleanup() + + def test_missing_setting_loads_as_empty(self): + self.assertEqual(self.service.load(), "") + + def test_save_normalizes_and_persists_serial(self): + serial = self.service.save(" 192.168.0.173:5555 ") + + self.assertEqual(serial, "192.168.0.173:5555") + self.assertEqual( + self.repository.get(SELECTED_ANDROID_SERIAL_KEY), serial + ) + + def test_invalid_serial_is_rejected(self): + for serial in ("", "USB 001", None): + with self.subTest(serial=serial): + with self.assertRaises(ValueError): + self.service.save(serial) + + def test_corrupted_setting_loads_as_empty(self): + self.repository.set(SELECTED_ANDROID_SERIAL_KEY, {"serial": "bad"}) + + self.assertEqual(self.service.load(), "") + + def test_delete_removes_only_selected_serial(self): + self.repository.set("android.pdd_package", "com.xunmeng.pinduoduo") + self.service.save("USB-001") + + self.assertTrue(self.service.delete()) + + self.assertIsNone(self.repository.get(SELECTED_ANDROID_SERIAL_KEY)) + self.assertEqual( + self.repository.get("android.pdd_package"), + "com.xunmeng.pinduoduo", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/client/test/test_settings_ui_event.py b/client/test/test_settings_ui_event.py index 72aab08..7ce6e59 100644 --- a/client/test/test_settings_ui_event.py +++ b/client/test/test_settings_ui_event.py @@ -17,6 +17,7 @@ from src.android_device_service import ( AndroidDeviceSearchError, ) from src.mock_admin_gateway import MockAdminGateway +from src.selected_android_device_service import SELECTED_ANDROID_SERIAL_KEY from src.settings_repository import SettingsRepository from src.settings_ui import AndroidDeviceRow, SettingsPage @@ -57,6 +58,34 @@ class SlowAndroidDeviceService: return [AndroidDevice("USB-001", "USB", "Pixel 8", "14")] +class ControlledSettingsRepository(SettingsRepository): + """控制 Android 设备设置写入速度和失败,其他设置保持正常。""" + + def __init__(self, db_path, delay=0.0): + super().__init__(db_path) + self.delay = delay + self.fail_selected_write = False + self.selected_write_count = 0 + + def set(self, setting_key, value, updated_at=None): + if setting_key == SELECTED_ANDROID_SERIAL_KEY: + self.selected_write_count += 1 + if self.delay: + time.sleep(self.delay) + if self.fail_selected_write: + raise OSError("模拟数据库写入失败") + return super().set(setting_key, value, updated_at) + + def delete(self, setting_key): + if setting_key == SELECTED_ANDROID_SERIAL_KEY: + self.selected_write_count += 1 + if self.delay: + time.sleep(self.delay) + if self.fail_selected_write: + raise OSError("模拟数据库删除失败") + return super().delete(setting_key) + + class SettingsPageEventTest(unittest.TestCase): @classmethod def setUpClass(cls): @@ -254,7 +283,7 @@ class SettingsPageEventTest(unittest.TestCase): page.deviceTableModel.device_for_serial("USB-OLD") ) self.assertIn("搜索失败:未找到 adb", page.deviceStatusLabel.text()) - self.assertIn("设备列表未更改", page.deviceStatusLabel.text()) + self.assertIn("已保存配置未更改", page.deviceStatusLabel.text()) page.eventBinder.shutdown() page.deleteLater() @@ -321,6 +350,186 @@ class SettingsPageEventTest(unittest.TestCase): ) page.deleteLater() + def test_selected_android_device_is_saved_to_sqlite(self): + page = SettingsPage( + settings_repository=self.repository, + admin_gateway=MockAdminGateway(), + ) + page.set_android_devices( + [AndroidDeviceRow("USB-001", "USB", "Pixel 8", "14")] + ) + page.deviceTableModel.set_checked_serial("USB-001") + + page.saveButton.click() + self.assertFalse(page.saveButton.isEnabled()) + self._wait_until( + lambda: page.eventBinder._android_setting_thread is None + ) + + self.assertEqual( + self.repository.get(SELECTED_ANDROID_SERIAL_KEY), "USB-001" + ) + self.assertEqual( + page.deviceStatusLabel.text(), + "当前使用设备:USB-001(已保存)", + ) + self.assertTrue(page.deleteButton.isEnabled()) + page.eventBinder.shutdown() + page.deleteLater() + + def test_saved_android_device_is_restored_before_search(self): + self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-001") + + page = SettingsPage( + settings_repository=self.repository, + admin_gateway=MockAdminGateway(), + ) + + self.assertEqual(page.eventBinder._saved_android_serial, "USB-001") + self.assertEqual(page.deviceTableModel.checked_serial, "") + self.assertIn("等待搜索确认连接", page.deviceStatusLabel.text()) + self.assertTrue(page.deleteButton.isEnabled()) + page.eventBinder.shutdown() + page.deleteLater() + + def test_search_auto_selects_saved_online_device(self): + self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-001") + service = SequenceAndroidDeviceService( + [[AndroidDevice("USB-001", "USB", "Pixel 8", "14")]] + ) + page = SettingsPage( + settings_repository=self.repository, + admin_gateway=MockAdminGateway(), + android_device_service=service, + ) + + page.searchButton.click() + self._wait_until(lambda: page.eventBinder._search_thread is None) + + self.assertEqual(page.deviceTableModel.checked_serial, "USB-001") + self.assertIn("已自动选择", page.deviceStatusLabel.text()) + page.eventBinder.shutdown() + page.deleteLater() + + def test_search_keeps_saved_device_when_it_is_offline(self): + self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-001") + service = SequenceAndroidDeviceService([[]]) + page = SettingsPage( + settings_repository=self.repository, + admin_gateway=MockAdminGateway(), + android_device_service=service, + ) + + page.searchButton.click() + self._wait_until(lambda: page.eventBinder._search_thread is None) + + self.assertEqual( + self.repository.get(SELECTED_ANDROID_SERIAL_KEY), "USB-001" + ) + self.assertEqual(page.deviceTableModel.checked_serial, "") + self.assertIn("已保存设备当前未连接", page.deviceStatusLabel.text()) + self.assertIn("配置已保留", page.deviceStatusLabel.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 + page = SettingsPage( + settings_repository=repository, + admin_gateway=MockAdminGateway(), + ) + page.set_android_devices( + [ + AndroidDeviceRow("USB-OLD", "USB"), + AndroidDeviceRow("USB-NEW", "USB"), + ] + ) + page.deviceTableModel.set_checked_serial("USB-NEW") + + page.saveButton.click() + self._wait_until( + lambda: page.eventBinder._android_setting_thread is None + ) + + self.assertEqual( + repository.get(SELECTED_ANDROID_SERIAL_KEY), "USB-OLD" + ) + self.assertEqual(page.deviceTableModel.checked_serial, "USB-NEW") + self.assertIn("原设备配置未更改", page.deviceStatusLabel.text()) + self.assertTrue(page.saveButton.isEnabled()) + page.eventBinder.shutdown() + page.deleteLater() + + def test_delete_clears_saved_offline_device_only(self): + self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-OFFLINE") + self.repository.set("android.pdd_package", "com.xunmeng.pinduoduo") + page = SettingsPage( + settings_repository=self.repository, + admin_gateway=MockAdminGateway(), + ) + + page.deleteButton.click() + self._wait_until( + lambda: page.eventBinder._android_setting_thread is None + ) + + self.assertIsNone( + self.repository.get(SELECTED_ANDROID_SERIAL_KEY) + ) + self.assertEqual( + self.repository.get("android.pdd_package"), + "com.xunmeng.pinduoduo", + ) + self.assertIn("本地配置已删除", page.deviceStatusLabel.text()) + self.assertFalse(page.deleteButton.isEnabled()) + page.eventBinder.shutdown() + page.deleteLater() + + def test_slow_android_device_save_does_not_block_or_start_twice(self): + repository = ControlledSettingsRepository(self.db_path, delay=0.08) + page = SettingsPage( + settings_repository=repository, + admin_gateway=MockAdminGateway(), + ) + page.set_android_devices([AndroidDeviceRow("USB-001", "USB")]) + page.deviceTableModel.set_checked_serial("USB-001") + timer_fired = [] + QTimer.singleShot(10, lambda: timer_fired.append(True)) + + page.saveButton.click() + page.eventBinder._request_save() + self._wait_until(lambda: bool(timer_fired), timeout_ms=500) + self._wait_until( + lambda: page.eventBinder._android_setting_thread is None + ) + + self.assertTrue(timer_fired) + self.assertEqual(repository.selected_write_count, 1) + page.eventBinder.shutdown() + page.deleteLater() + + def test_shutdown_ignores_late_android_device_save_result(self): + repository = ControlledSettingsRepository(self.db_path, delay=0.08) + page = SettingsPage( + settings_repository=repository, + admin_gateway=MockAdminGateway(), + ) + page.set_android_devices([AndroidDeviceRow("USB-001", "USB")]) + page.deviceTableModel.set_checked_serial("USB-001") + + page.saveButton.click() + self._wait_until( + lambda: page.eventBinder._android_setting_thread is not None + ) + status_before_close = page.deviceStatusLabel.text() + page.eventBinder.shutdown() + QTest.qWait(120) + + self.assertEqual(page.deviceStatusLabel.text(), status_before_close) + page.deleteLater() + if __name__ == "__main__": unittest.main()