feat: 保存当前使用的 Android 设备 (#22)

This commit is contained in:
chengma
2026-08-07 14:13:40 +08:00
parent d2de7331bb
commit 27a09e524c
4 changed files with 558 additions and 17 deletions
@@ -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
+238 -16
View File
@@ -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)