feat: 保存当前使用的 Android 设备 (#22)
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user