diff --git a/整体开发需求/03_移动端接入需求.md b/整体开发需求/03_移动端接入需求.md index 8f73197..a629a55 100644 --- a/整体开发需求/03_移动端接入需求.md +++ b/整体开发需求/03_移动端接入需求.md @@ -26,3 +26,7 @@ DEPLOY-MOBILE-BLE/DEVICE-MOBILE-LIFECYCLE 的升级约束:包含移动端安 ## 2026-09-26 接口修订 DEVICE-MOBILE-CONTROL:BLE library.list 与网页共用 LibraryOrderStore 的用户混合顺序,演示条目保持固定位置;顺序参与分页修订。 DEVICE-MOBILE-SETTINGS:WiFi 激活任务可选 error_code 为 AUTH_FAILED、NETWORK_UNAVAILABLE、IP_CONFIG_FAILED、UNKNOWN;原因来自 NetworkManager 的结构化状态,仅可靠认证证据可报告 AUTH_FAILED,不输出原始命令错误或密码。 + +## 2026-09-27 手机界面测试补充 + +TEST-MOBILE-INTEGRATION:Android 顶栏溢出循环、扫描条目按认证会话切换断开且重新扫描保留、两处预览居中,按 ANDROID-TEST-STATE/UI 做共享逻辑与 root 真机布局验收。设备接口、广播仲裁与驱动不变,不需要设备部署或实屏视觉检查。 diff --git a/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md b/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md index 7c474f4..5a8d585 100644 --- a/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md +++ b/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md @@ -279,3 +279,7 @@ nmcli -t -f GENERAL.STATE,IP4.ADDRESS,IP4.GATEWAY device show wlan0 在含中文和空格的隔离目录测试 `prepare_credentials.py`:私有文件缺失时逐字复制示例、返回非零并要求用户填写;占位符、空值和缺字段继续失败;填写完整后成功;已有文件在所有路径均保持字节不变。随后检查文本中的 Windows/macOS/Linux 开发者主目录、当前用户名和主机名、私钥头、高可信秘密及非测试归档中的私有 IPv4;对大文件流式检查 ASCII、UTF-8 和 UTF-16LE 主机标识。测试假值、回环地址、硬件地址、设备固定部署路径和正式 IMG README 的公开默认值不得误报。 执行 `git diff --check`、`python -m pytest -q tests/test_repository_hygiene.py tests/test_workspace_layout.py` 和本机 `all` 套件。最终用 `git check-ignore -v` 复核私有文件由根规则命中,并确认 `git status --short --ignored` 只把秘密、本机状态、运行数据和可再生缓存列为忽略项;使用 `git lfs track` 与 `git lfs ls-files` 核对已配置和已暂存的 LFS 对象。本测试不执行 commit 或 push。 + +## Android 0.2.1 界面专项映射 + +TEST-MOBILE-INTEGRATION 增补 MOBILE-STATUS/DISCOVERY/PREVIEW 对应的 ANDROID-TEST-STATE/UI:顶栏溢出循环配置和完整语义、认证连接后的条目断开及扫描保留、两处预览按卡片居中且刷新不重叠。共享逻辑及登记手机root验收入口和合格标准维护在 `移动端相关内容/安卓app/如何安卓测试/README.md`。手机界面及会话显示状态变化没有修改设备服务或驱动,实屏视觉检查不适用,不增加接线、显示图案切换、网络切换或真实systemd停启步骤。 diff --git a/移动端相关内容/安卓app/如何安卓测试/README.md b/移动端相关内容/安卓app/如何安卓测试/README.md index f1eda1f..0d1ba40 100644 --- a/移动端相关内容/安卓app/如何安卓测试/README.md +++ b/移动端相关内容/安卓app/如何安卓测试/README.md @@ -113,3 +113,11 @@ ANDROID-TEST-UI 增加四栏、三点菜单、昵称、长按保存设备、WiFi - root 辅助脚本保持 LF 换行;文本通过 App 内临时剪贴板和 root 按键输入,结束清理,不把密码放在命令参数中。 排障指南按症状记录 root 命令解析、LF 换行、布局进程重建、语义定位、表单输入、安装权限、BLE 阶段释放及 WiFi 恢复。遇到这些情况先查 [对应解法](常见问题与排障.md),不得重复试用已知不可靠的输入或转储方法。 + +### 0.2.1 顶栏、连接条目与居中验收 + +ANDROID-TEST-STATE:共享逻辑验证自动/手动连接后关联、重新扫描保留、旧回调隔离、切换和失败/异常清理。ANDROID-TEST-UI:运行 run_ui_layout_test.py,在每组字体/方向下连接真实目标,按稳定语义资源标签检查连接条目断开、再次扫描保留、实际断开和再连;两页预览图片与标题中心距卡片中心不超过3物理像素,刷新位于右侧且无重叠。所有输入通过 root;测试末尾主动断开,运行器恢复布局设置。 + +顶栏保留完整文字语义,菜单位于固定区域;长文本循环参数由共享实现及编译核验,语义边界不证明文字实际运动,不记为目视验收。未取得本次视觉指示时不截图、不启动摄像头。纯手机界面调整的实屏视觉检查不适用,不需切网、显示内容切换或驱动测试。 + +短视口连接列表验收使用 connection_list 的实时可见边界,以 root 滑动定位扫描条目,按当前条目容器内的资源标签选连接/断开,不能假设设备名称始终在首屏。布局恢复须同时回读密度覆盖、字体与旋转;运行器输出恢复核验结果。 diff --git a/移动端相关内容/安卓app/如何安卓测试/run_ble_test.py b/移动端相关内容/安卓app/如何安卓测试/run_ble_test.py index f5889a5..1a60148 100644 --- a/移动端相关内容/安卓app/如何安卓测试/run_ble_test.py +++ b/移动端相关内容/安卓app/如何安卓测试/run_ble_test.py @@ -133,9 +133,13 @@ def main(): if layout_original: root("am force-stop org.qimiaoscreen.controller") root("wm density " + layout_density) + restored_density = re.search(r"Override density: (\d+)", root("wm density")) + restored_density = restored_density[1] if restored_density else "reset" + if restored_density != layout_density: raise RuntimeError("Layout density restore failed") for key, value in layout_original.items(): root("settings delete system " + key if value == "null" else "settings put system " + key + " " + value) if root("settings get system " + key) != value: raise RuntimeError("Layout settings restore failed") + print("Layout settings restored and verified", flush=True) if args.test == "layout": adb("shell", "rm", "-f", "/data/local/tmp/qms-authorized-root.sh", quiet=True) if args.toggle_bluetooth: diff --git a/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md b/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md index e640a64..fe55e52 100644 --- a/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md +++ b/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md @@ -61,3 +61,11 @@ Compose 的禁用语义可能位于父节点,文字节点自身仍 enabled。 每轮检查:本轮有哪些失败/调整、是否已有可重复防范步骤、恢复是否核验、哪些原因仍未知。已验证解法更新本页和对应流程/构建说明;一次性证据留归档,不能将未证实推测写成固定根因。 最终反馈写明经验已更新的文档,以及尚未解决的问题或未验证边界。若没有新增经验,说明沿用现有方法即可,不复制重复条目。遵守根协作规则:不写真实凭据、手机标识、开发机绝对路径,文档更新后执行仓库卫生检查。 + +## 12. 轮询或滑动后节点失效、名称可见但按钮无法点击 + +本轮三种布局已验证:读取语义节点期间可能出现 StaleObjectException,应在原有有界期限内丢弃旧节点并重新查找,不复用旧坐标、不放宽断言。滚动定位使用 connection_list 的实时可见边界;扫描按钮还要限制在目标设备条目容器,检查按钮启用且 visibleBounds 非空。名称露出不证明兄弟按钮可见,空边界不能取中心点点击。 + +滑动后通过两次间隔400毫秒的实时边界相等检查确认目标停稳(最多5秒),然后仅发送一次 root 点击;不因未看到中间状态而重复点击连接。记录初次连接、连接中扫描、两处预览、条目断开、手动重连等阶段。短暂“连接中”未被观测不代表未发起请求,终态以认证连接和条目状态核验为准。 + +本轮失败、恢复与最终结果见 `../测试结果归档/2026-09-27_界面细节优化/`;重测通过不证明此前连接未出现的唯一根因。字体、旋转和密度恢复后均回读核验。 diff --git a/移动端相关内容/安卓app/安卓开发详细设计.md b/移动端相关内容/安卓app/安卓开发详细设计.md index dd91a91..7ec497b 100644 --- a/移动端相关内容/安卓app/安卓开发详细设计.md +++ b/移动端相关内容/安卓app/安卓开发详细设计.md @@ -68,3 +68,7 @@ BlueZ D-Bus 接入作为可恢复模块,单独管理连接/广播,不在 ## 2026-09-26 四栏实现修订 采用稳定页面编号 0连接、1设备状态、2显示内容、3设备设置;保存设备完整 ID、昵称、短编号、最近连接时间,不保存 MAC。扫描状态与连接独立,旧回调按代数隔离;RSSI 由平台 DeviceLink 可选方法读取。顶部共用 WiFi 任务状态。菜单、折叠表单、确认弹窗使用共享 Compose;版本元数据从 Android BuildConfig 注入。非敏感 WiFi 草稿跨页保留,密码仅当前编辑驻留,离页或提交清除。原三页/JSON详情/刷新率与性能入口描述由此节替代。 + +## 2026-09-27 界面修订 + +共享状态增加仅当前认证会话有效的 connectedDevice 扫描快照,以本次句柄关联按钮,持久身份仍使用完整设备 ID。扫描重置保留它,重复广播按句柄去重;所有会话结束清空关联。顶栏使用共享 Compose 跑马灯,只在溢出时无限循环,文本变化重启。两处预览复用居中卡片,标题按刷新按钮实际宽度预留对称空间。App 0.2.1/versionCode 4,仅 debug。 diff --git a/移动端相关内容/安卓app/安卓程序源代码/androidApp/build.gradle.kts b/移动端相关内容/安卓app/安卓程序源代码/androidApp/build.gradle.kts index f1c93de..d98cd15 100644 --- a/移动端相关内容/安卓app/安卓程序源代码/androidApp/build.gradle.kts +++ b/移动端相关内容/安卓app/安卓程序源代码/androidApp/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "org.qimiaoscreen.controller" minSdk = 26 targetSdk = 37 - versionCode = 3 - versionName = "0.2.0" + versionCode = 4 + versionName = "0.2.1" buildConfigField("String", "BUILD_DATE", "\"" + ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm XXX")) + "\"") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt b/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt index bbbbaad..da89baf 100644 --- a/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt +++ b/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt @@ -68,7 +68,7 @@ class AppLifecycleTest { click(ui.findObject(By.text("连接"))) click(ui.findObject(By.text("⋮"))) click(checkNotNull(ui.wait(Until.findObject(By.text("关于")), 5_000))) - assertTrue(ui.wait(Until.hasObject(By.textContains("版本 0.2.0")), 5_000)) + assertTrue(ui.wait(Until.hasObject(By.textContains("版本 " + BuildConfig.VERSION_NAME)), 5_000)) click(ui.findObject(By.text("关闭"))) click(ui.findObject(By.text("⋮"))) click(checkNotNull(ui.wait(Until.findObject(By.text("软件设置")), 5_000))) diff --git a/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/LayoutTest.kt b/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/LayoutTest.kt index cc628b0..bfdd6e4 100644 --- a/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/LayoutTest.kt +++ b/移动端相关内容/安卓app/安卓程序源代码/androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/LayoutTest.kt @@ -3,13 +3,16 @@ package org.qimiaoscreen.controller import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.StaleObjectException import androidx.test.uiautomator.Until import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import kotlin.math.abs /** Read semantic bounds; inject all input through the authorized root helper. */ @RunWith(AndroidJUnit4::class) @@ -23,19 +26,126 @@ class LayoutTest { check(ui.executeShellCommand("sh /data/local/tmp/qms-authorized-input.sh $command").contains("QMS_INPUT_OK")) Thread.sleep(250) } + fun visible(selector: BySelector, timeout: Long, scroll: Boolean = false, + accept: (UiObject2) -> Boolean = { true }): UiObject2? { + val deadline = android.os.SystemClock.elapsedRealtime() + timeout + var attempts = 0 + while (android.os.SystemClock.elapsedRealtime() < deadline) { + try { + ui.findObjects(selector).firstOrNull { !it.visibleBounds.isEmpty && accept(it) }?.let { return it } + val list = if (scroll) ui.findObject(By.res("connection_list")) else null + if (list != null && !list.visibleBounds.isEmpty) { + check(list.applicationPackage == "org.qimiaoscreen.controller") + val b = list.visibleBounds + check(b.left >= 0 && b.top >= 0 && b.right <= ui.displayWidth && b.bottom <= ui.displayHeight) + val upper = b.top + b.height() / 5 + val lower = b.bottom - b.height() / 5 + val down = attempts++ % 10 < 5 + input("swipe ${b.centerX()} ${if (down) lower else upper} ${b.centerX()} ${if (down) upper else lower} 350") + } + } catch (_: StaleObjectException) { + // Polling or scrolling replaced the semantic tree. Discard it + // and resolve fresh nodes; never reuse the old coordinates. + } + Thread.sleep(250) + } + return null + } + fun stableTap(resolve: () -> UiObject2?) { + val deadline = android.os.SystemClock.elapsedRealtime() + 5_000 + var previous: android.graphics.Rect? = null + while (android.os.SystemClock.elapsedRealtime() < deadline) { + try { + val node = resolve() + if (node != null) { + check(node.applicationPackage == "org.qimiaoscreen.controller") + val b = node.visibleBounds + if (!b.isEmpty && b.left >= 0 && b.top >= 0 && b.right <= ui.displayWidth && b.bottom <= ui.displayHeight) { + if (b == previous) { input("tap ${b.centerX()} ${b.centerY()}"); return } + previous = android.graphics.Rect(b) + } else previous = null + } else previous = null + } catch (_: StaleObjectException) { previous = null } + Thread.sleep(400) + } + error("Root target did not obtain stable visible bounds") + } fun click(label: String) { - check(ui.wait(Until.hasObject(By.text(label)), 15_000)) { "Layout control unavailable: $label" } - val node = checkNotNull(ui.findObjects(By.text(label)).filter { !it.visibleBounds.isEmpty } - .maxByOrNull { it.visibleCenter.y }) { "Visible layout target unavailable: $label" } - check(node.applicationPackage == "org.qimiaoscreen.controller") - val bounds = node.visibleBounds - assertTrue(bounds.left >= 0 && bounds.top >= 0 && bounds.right <= ui.displayWidth && bounds.bottom <= ui.displayHeight) - val point = node.visibleCenter - input("tap ${point.x} ${point.y}") + check(visible(By.text(label), 15_000, label == "扫描设备") != null) { "Layout control unavailable: $label" } + stableTap { ui.findObjects(By.text(label)).filter { !it.visibleBounds.isEmpty }.maxByOrNull { it.visibleCenter.y } } } ui.executeShellCommand("am start -W -n org.qimiaoscreen.controller/.MainActivity") assertTrue(ui.wait(Until.hasObject(By.pkg("org.qimiaoscreen.controller")), 10_000)) for (label in listOf("连接", "设备状态", "显示内容", "设备设置", "连接")) click(label) click("⋮"); click("关于"); click("关闭") + fun tagged(tag: String): UiObject2 = checkNotNull(visible(By.res(tag), 35_000, tag == "connected_scan_disconnect")) { + "Required App semantic tag unavailable: $tag" + } + fun tap(node: UiObject2) { + val tag = checkNotNull(node.resourceName) + stableTap { ui.findObjects(By.res(tag)).firstOrNull { !it.visibleBounds.isEmpty && it.isEnabled } } + } + fun stage(name: String) = instrumentation.sendStatus(2, android.os.Bundle().apply { putString("stream", "Layout stage: $name\n") }) + fun connectScanned() { + click("扫描设备") + val name = InstrumentationRegistry.getArguments().getString("device_name")!! + checkNotNull(visible(By.res("scan_connect"), 20_000, scroll = true) { + it.isEnabled && it.parent?.findObject(By.text(name)) != null + }) { "Visible target scan button unavailable" } + stableTap { ui.findObjects(By.res("scan_connect")).firstOrNull { + !it.visibleBounds.isEmpty && it.isEnabled && it.parent?.findObject(By.text(name)) != null + } } + stage("connect-request-observed=" + ui.hasObject(By.textContains("连接中"))) + val deadline = android.os.SystemClock.elapsedRealtime() + 35_000 + while (!ui.hasObject(By.res("connected_scan_disconnect")) && android.os.SystemClock.elapsedRealtime() < deadline) { + val failureText = ui.findObject(By.res("controller_error"))?.text + if (failureText != null) { + val category = listOf("超时", "蓝牙连接已中断", "蓝牙连接失败", "身份", "不兼容", "会话").firstOrNull { failureText.contains(it) } ?: "未分类" + error("Connection failed after root input: $category") + } + Thread.sleep(250) + } + tagged("connected_scan_disconnect") + } + try { + stage("initial-connect") + if (visible(By.res("connected_scan_disconnect"), 35_000, scroll = true) == null) connectScanned() + val summary = tagged("connection_summary") + assertTrue(summary.text.startsWith("当前连接设备:")) + assertTrue(summary.text.contains(" · ")) + assertTrue(summary.visibleBounds.right <= ui.findObject(By.text("⋮")).visibleBounds.left) + stage("scan-while-connected"); click("扫描设备") + tagged("connected_scan_disconnect") + Thread.sleep(13_000) + assertTrue(ui.hasObject(By.res("connected_scan_disconnect"))) + for (page in listOf("设备状态", "显示内容")) { + stage("frame-" + if (page == "设备状态") "status" else "content") + click(page) + tagged("frame_image") + Thread.sleep(500) + val card = tagged("frame_card").visibleBounds + val title = tagged("frame_title").visibleBounds + val image = tagged("frame_image").visibleBounds + val refresh = tagged("frame_refresh").visibleBounds + assertTrue("Title must share card center", abs(title.centerX() - card.centerX()) <= 3) + assertTrue("Image must share card center", abs(image.centerX() - card.centerX()) <= 3) + assertTrue("Title and refresh must not overlap", title.right <= refresh.left) + assertTrue(refresh.centerX() > card.centerX() && refresh.right <= card.right) + assertTrue(image.left >= card.left && image.right <= card.right) + tap(tagged("frame_refresh")) + } + click("连接") + stage("disconnect-row") + tap(tagged("connected_scan_disconnect")) + assertTrue(ui.wait(Until.gone(By.text("断开连接")), 10_000)) + assertTrue(ui.wait(Until.gone(By.res("connected_scan_disconnect")), 10_000)) + stage("manual-reconnect"); connectScanned() + stage("final-disconnect") + tap(tagged("connected_scan_disconnect")) + assertTrue(ui.wait(Until.gone(By.text("断开连接")), 10_000)) + } finally { + click("连接") + visible(By.res("connected_scan_disconnect"), 5_000, scroll = true)?.let { tap(it) } + } } } diff --git a/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt b/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt index e832aba..b365a20 100644 --- a/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt +++ b/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt @@ -27,6 +27,7 @@ data class ControllerState( val page: Int = 0, val phase: String = "未连接", val scanning: Boolean = false, val busy: Boolean = false, val error: String? = null, val clientName: String = "", val devices: List = emptyList(), val identity: JsonObject? = null, + val connectedDevice: DiscoveredDevice? = null, val status: JsonObject? = null, val storage: JsonObject? = null, val settings: JsonObject? = null, val wifi: JsonObject? = null, val library: List = emptyList(), val networks: List = emptyList(), @@ -88,7 +89,7 @@ class Controller(private val platform: MobilePlatform, private val scope: Corout scanJob?.cancel() platform.stopScan() val generation = ++scanGeneration - change { it.copy(scanning = true, devices = emptyList(), error = null, phase = if (it.identity != null) it.phase else if (auto) "正在重连上次设备" else "正在扫描") } + change { it.copy(scanning = true, devices = listOfNotNull(it.connectedDevice), error = null, phase = if (it.identity != null) it.phase else if (auto) "正在重连上次设备" else "正在扫描") } val lastId = platform.load("last_device_id")?.takeIf { it.isNotBlank() && it !in forgottenIds() } val short = lastId?.replace("-", "")?.take(8) platform.scan({ device -> scope.launch { @@ -108,6 +109,7 @@ class Controller(private val platform: MobilePlatform, private val scope: Corout } } fun connect(device: DiscoveredDevice, expectedId: String? = null) { + if (state.value.identity != null && state.value.connectedDevice?.handle == device.handle) return if (state.value.identity?.get("device_id")?.jsonPrimitive?.content == expectedId && expectedId != null) return if (connectionJob?.isActive == true || rpc != null) disconnect() scanGeneration++; scanJob?.cancel(); platform.stopScan() @@ -120,6 +122,7 @@ class Controller(private val platform: MobilePlatform, private val scope: Corout activeLink = platform.connect(device.handle) pending = DeviceRpc(checkNotNull(activeLink), platform.crypto()) val identity = pending.open(state.value.clientName) + if (generation != connectionGeneration) { pending.close(); return@launch } val id = identity.getValue("device_id").jsonPrimitive.content require(expectedId == null || expectedId == id) { "设备身份与上次连接不一致" } require(identity.getValue("protocol_major").jsonPrimitive.int == 1) { "通信协议不兼容" } @@ -129,14 +132,16 @@ class Controller(private val platform: MobilePlatform, private val scope: Corout persistRemembered((state.value.remembered.filterNot { it.id == id } + RememberedDevice(id, identity.getValue("device_name").jsonPrimitive.content, identity.getValue("short_id").jsonPrimitive.content, Clock.System.now().toEpochMilliseconds())).sortedByDescending { it.lastConnected }) - change { it.copy(identity = identity, phase = "已连接", signal = device.signal) } + change { it.copy(identity = identity, connectedDevice = device, + devices = it.devices.filterNot { old -> old.handle == device.handle } + device, + phase = "已连接", signal = null) } refreshAll() startPolling() } catch (e: Exception) { pending?.close() if (generation == connectionGeneration) { rpc = null; activeLink = null - if (e !is CancellationException) change { it.copy(identity = null, phase = "未连接", error = e.message ?: "连接失败") } + if (e !is CancellationException) change { it.copy(identity = null, connectedDevice = null, phase = "未连接", error = e.message ?: "连接失败") } } } } diff --git a/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt b/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt index 272119a..26cdeaf 100644 --- a/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt +++ b/移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt @@ -12,11 +12,12 @@ private class SimulatedPlatform : MobilePlatform { val preferences = mutableMapOf() val links = mutableListOf() val scanCallbacks = mutableListOf<(DiscoveredDevice) -> Unit>() + var signal: Deferred? = null override fun load(key: String) = preferences[key] override fun save(key: String, value: String) { preferences[key] = value } override fun scan(onDevice: (DiscoveredDevice) -> Unit, onError: (String) -> Unit) { scanCallbacks += onDevice } override fun stopScan() {} - override suspend fun connect(handle: String) = SimulatedLink(if (handle == "second-handle") "11111111-0000-4000-8000-000000000001" else "00000000-0000-4000-8000-000000000001").also { links += it } + override suspend fun connect(handle: String) = SimulatedLink(if (handle == "second-handle") "11111111-0000-4000-8000-000000000001" else "00000000-0000-4000-8000-000000000001", signal).also { links += it } override fun crypto() = object : SecureChannel { override val hello = byteArrayOf(1) override fun finish(serverHello: ByteArray) {} @@ -25,7 +26,7 @@ private class SimulatedPlatform : MobilePlatform { } } -private class SimulatedLink(private val deviceId: String) : DeviceLink { +private class SimulatedLink(private val deviceId: String, private val signal: Deferred?) : DeviceLink { override val mtu = 247 private val receiver = Reassembler() private val messages = Channel(Channel.UNLIMITED) @@ -60,6 +61,7 @@ private class SimulatedLink(private val deviceId: String) : DeviceLink { Protocol.encode(if (kind == 1) 2 else 3, sequence++, response, mtu).forEach { messages.send(it) } } override suspend fun receive() = messages.receive() + override suspend fun readSignal(): Int? = signal?.await() override fun close() { closed = true; messages.close() } } @@ -82,6 +84,7 @@ class ControllerRecoveryTest { assertNull(controller.state.value.frame) assertNull(controller.state.value.frameCheckedAtMs) assertNull(controller.state.value.storage) + assertNull(controller.state.value.connectedDevice) assertTrue(platform.links.last().closed) controller.connect(device) withTimeout(2_000) { controller.state.first { it.status != null } } @@ -135,11 +138,15 @@ class ControllerRecoveryTest { assertEquals("模拟屏", remembered.name) controller.scan() assertNotNull(controller.state.value.identity) + assertEquals(device, controller.state.value.connectedDevice) + assertEquals(listOf(device), controller.state.value.devices) assertTrue(controller.state.value.scanning) platform.scanCallbacks.last()(device) withTimeout(2_000) { controller.state.first { it.devices.isNotEmpty() } } assertEquals(1, platform.links.size) controller.disconnect() + assertNull(controller.state.value.connectedDevice) + assertTrue(controller.state.value.devices.isEmpty()) assertEquals(remembered.id, Controller(platform, scope).state.value.remembered.single().id) controller.forget(remembered.id) controller.foreground(true) @@ -164,6 +171,8 @@ class ControllerRecoveryTest { assertTrue(old.closed) assertEquals(2, platform.links.size) assertEquals(2, controller.state.value.remembered.size) + assertEquals("second-handle", controller.state.value.connectedDevice?.handle) + assertFalse(controller.state.value.devices.any { it.handle == device.handle }) controller.forget("11111111-0000-4000-8000-000000000001") assertNull(controller.state.value.identity) assertTrue(platform.links.last().closed) @@ -181,6 +190,37 @@ class ControllerRecoveryTest { platform.scanCallbacks.last()(device.copy(name = "QMS-00000000")) withTimeout(2_000) { controller.state.first { it.status != null } } assertEquals(id, controller.state.value.remembered.single().id) + assertEquals(device.handle, controller.state.value.connectedDevice?.handle) + assertEquals(1, controller.state.value.devices.size) + } finally { controller.disconnect(); scope.cancel() } + } + + @Test fun connectedSignalWaitsForLiveReadInsteadOfUsingScanSnapshot() = runBlocking { + val platform = SimulatedPlatform() + val signal = CompletableDeferred() + platform.signal = signal + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val controller = Controller(platform, scope) + try { + controller.connect(device) + withTimeout(2_000) { controller.state.first { it.identity != null } } + assertNull(controller.state.value.signal) + signal.complete(-55) + withTimeout(2_000) { controller.state.first { it.status != null } } + assertEquals(-55, controller.state.value.signal) + } finally { controller.disconnect(); scope.cancel() } + } + + @Test fun failedHandshakeCannotKeepConnectedScanEntry() = runBlocking { + val platform = SimulatedPlatform() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val controller = Controller(platform, scope) + try { + controller.connect(device, "unexpected-device-id") + withTimeout(2_000) { controller.state.first { it.error != null } } + assertNull(controller.state.value.identity) + assertNull(controller.state.value.connectedDevice) + assertTrue(platform.links.single().closed) } finally { controller.disconnect(); scope.cancel() } } diff --git a/移动端相关内容/安卓app/安卓程序源代码/sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt b/移动端相关内容/安卓app/安卓程序源代码/sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt index 85cee65..f15d958 100644 --- a/移动端相关内容/安卓app/安卓程序源代码/sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt +++ b/移动端相关内容/安卓app/安卓程序源代码/sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt @@ -7,6 +7,13 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -21,6 +28,7 @@ private fun JsonObject.text(key: String, fallback: String = "—") = get(key)?.j data class AppInfo(val version: String, val build: String, val date: String) +@OptIn(ExperimentalFoundationApi::class) @Composable fun QimiaoApp(controller: Controller, onScan: () -> Unit, decode: (String) -> ImageBitmap?, appInfo: AppInfo = AppInfo("—", "—", "—")) { @@ -34,12 +42,16 @@ fun QimiaoApp(controller: Controller, onScan: () -> Unit, decode: (String) -> Im icon = { Text(listOf("◉", "▣", "▦", "⚙")[index]) }, label = { Text(name) }) } } }) { padding -> - Column(Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column(Modifier.fillMaxSize().semantics { testTagsAsResourceId = true }.padding(padding).padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { val identity = state.identity - Text(if (identity == null) "当前连接设备:未连接${if (state.phase == "正在连接") " · 连接中" else ""}" - else "当前连接设备:${identity.text("device_name")}(${identity.text("short_id")}) · ${wifiSummary(state)}", - Modifier.weight(1f), style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis) + val summary = if (identity == null) "当前连接设备:未连接${if (state.phase == "正在连接") " · 连接中" else ""}" + else "当前连接设备:${identity.text("device_name")}(${identity.text("short_id")}) · ${wifiSummary(state)}" + key(summary) { Text(summary, + Modifier.weight(1f).testTag("connection_summary").clipToBounds() + .basicMarquee(iterations = Int.MAX_VALUE, repeatDelayMillis = 1_000, + initialDelayMillis = 1_000, velocity = 30.dp), + style = MaterialTheme.typography.bodySmall, maxLines = 1, softWrap = false, overflow = TextOverflow.Clip) } Box { TextButton({ menu = true }) { Text("⋮") } DropdownMenu(menu, { menu = false }) { @@ -50,7 +62,7 @@ fun QimiaoApp(controller: Controller, onScan: () -> Unit, decode: (String) -> Im } if (state.busy) LinearProgressIndicator(Modifier.fillMaxWidth()) state.error?.let { error -> Card { Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { - Text(error, Modifier.weight(1f)); TextButton(controller::dismissError) { Text("知道了") } + Text(error, Modifier.weight(1f).testTag("controller_error")); TextButton(controller::dismissError) { Text("知道了") } } } } when (state.page) { 0 -> ConnectionPage(state, controller, onScan) @@ -79,7 +91,7 @@ private fun ConnectionPage(state: ControllerState, c: Controller, scan: () -> Un var selected by remember { mutableStateOf(null) } var renaming by remember { mutableStateOf(false) } var name by remember { mutableStateOf("") } - LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) { + LazyColumn(Modifier.fillMaxWidth().testTag("connection_list"), verticalArrangement = Arrangement.spacedBy(10.dp)) { item { Text("已保存的设备", style = MaterialTheme.typography.titleMedium) } if (state.remembered.isEmpty()) item { Text("暂无已保存设备,扫描后选择即可连接") } items(state.remembered, key = { it.id }) { device -> @@ -96,12 +108,16 @@ private fun ConnectionPage(state: ControllerState, c: Controller, scan: () -> Un } } item { Text("扫描到的设备", style = MaterialTheme.typography.titleMedium) } items(state.devices, key = { it.handle }) { device -> Card(Modifier.fillMaxWidth()) { + val connected = state.identity != null && state.connectedDevice?.handle == device.handle Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) { Column(Modifier.weight(1f)) { - Text(device.name) - Text("信号 ${device.signal} dBm${if (c.rememberedFor(device) != null) " · 已记忆" else ""}", style = MaterialTheme.typography.bodySmall) + Text(if (connected) state.identity!!.text("device_name") else device.name) + Text(if (connected) "已连接 · 蓝牙信号:${state.signal?.let { "$it dBm" } ?: "暂不可用"}" + else "扫描信号 ${device.signal} dBm${if (c.rememberedFor(device) != null) " · 已记忆" else ""}", style = MaterialTheme.typography.bodySmall) } - Button({ c.connect(device, c.rememberedFor(device)?.id) }, enabled = state.phase != "正在连接") { Text("连接") } + Button({ if (connected) c.disconnect() else c.connect(device, c.rememberedFor(device)?.id) }, + Modifier.testTag(if (connected) "connected_scan_disconnect" else "scan_connect"), + enabled = state.phase != "正在连接") { Text(if (connected) "断开" else "连接") } } } } if (state.identity != null) item { Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -130,13 +146,17 @@ private fun Frame(encoded: String?, checkedAt: Long?, decode: (String) -> ImageB val bitmap = remember(encoded) { encoded?.let(decode) } var now by remember { mutableLongStateOf(Clock.System.now().toEpochMilliseconds()) } LaunchedEffect(checkedAt) { while (true) { now = Clock.System.now().toEpochMilliseconds(); delay(1_000) } } - Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp)) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("设备画面", Modifier.weight(1f), style = MaterialTheme.typography.titleMedium) - TextButton(refresh, enabled = enabled) { Text("手动刷新") } + val density = LocalDensity.current + var refreshWidth by remember { mutableStateOf(96.dp) } + Card(Modifier.fillMaxWidth().testTag("frame_card")) { Column(Modifier.fillMaxWidth().padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.fillMaxWidth()) { + Text("设备画面", Modifier.fillMaxWidth().padding(horizontal = refreshWidth).align(Alignment.Center).testTag("frame_title"), + style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center) + TextButton(refresh, Modifier.align(Alignment.CenterEnd).testTag("frame_refresh") + .onSizeChanged { refreshWidth = with(density) { it.width.toDp() } }, enabled = enabled) { Text("手动刷新") } } - if (bitmap != null) Image(bitmap, "设备当前输出帧", Modifier.size(160.dp)) else Text("等待画面") - if (checkedAt != null && now - checkedAt > 6_000) Text("画面可能已过期,请刷新", style = MaterialTheme.typography.bodySmall) + if (bitmap != null) Image(bitmap, "设备当前输出帧", Modifier.size(160.dp).testTag("frame_image")) else Text("等待画面", textAlign = TextAlign.Center) + if (checkedAt != null && now - checkedAt > 6_000) Text("画面可能已过期,请刷新", style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center) } } } diff --git a/移动端相关内容/安卓app/构建与发布/README.md b/移动端相关内容/安卓app/构建与发布/README.md index 91256d4..bd5fe24 100644 --- a/移动端相关内容/安卓app/构建与发布/README.md +++ b/移动端相关内容/安卓app/构建与发布/README.md @@ -60,3 +60,5 @@ Gradle Kotlin DSL 里 `java` 可能被插件扩展名遮蔽;构建日期实现 Python 在 Windows 缺少 IANA tzdata 时,固定北京时间时间戳可使用标准库 `timezone(timedelta(hours=8))`;这一方法只适用于明确要求的固定 UTC+08:00 时间,不当作任意地区夏令时转换方案,也不因该情况自动新增依赖。 打包前重新核对隔离源码、实际 APK、构建日期和签名摘要;测试源码调整也要同步隔离工程,不能测试旧测试 APK。测试/构建遇到的新问题在完成后更新相应文档并在最终反馈说明,具体输入和真机排障见 [测试排障指南](../如何安卓测试/常见问题与排障.md)。 + +Android 0.2.1 / versionCode 4:顶栏长文本循环、连接条目断开/扫描保留、居中预览;仅debug签名。交付和本轮覆盖范围见交付包0.2.1-4及测试结果归档2026-09-27_界面细节优化。 diff --git a/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/README.md b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/README.md new file mode 100644 index 0000000..5adc6c9 --- /dev/null +++ b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/README.md @@ -0,0 +1,11 @@ +# 奇妙小屏幕 0.2.1 debug(versionCode 4) + +仅 debug 签名,可覆盖安装相同调试证书的旧包。Android 8.0 以上,真实验证机为 Android 13。 + +本包为当前开发验证产物;验收范围和未验证项见 `../../../测试结果归档/实施状态.md`。不能将 APK 成功生成视为所有首版验收通过。设备需已部署移动控制功能,历史设备版本号本身不代表支持 BLE。 + +SHA-256:`0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef`。源码摘要与签名证书摘要见 manifest.json。 + +## 本轮验证 + +JVM15项、root真机三组布局、后台恢复、BLE只读回归通过,已安装APK摘要与本包一致。完整证据与边界见 [本轮报告](../../../测试结果归档/2026-09-27_界面细节优化/README.md)。顶栏循环、扫描条目断开与保留、两页预览居中;仅debug签名。 diff --git a/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/SHA256SUMS b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/SHA256SUMS new file mode 100644 index 0000000..3653231 --- /dev/null +++ b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/SHA256SUMS @@ -0,0 +1 @@ +0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef 奇妙小屏幕-0.2.1-debug.apk diff --git a/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/manifest.json b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/manifest.json new file mode 100644 index 0000000..e208259 --- /dev/null +++ b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/manifest.json @@ -0,0 +1,142 @@ +{ + "app_version": "0.2.1", + "version_code": 4, + "signing": "debug", + "build_date": "2026-09-27 20:28 +08:00", + "status": "development-validation-see-test-report", + "apk_sha256": "0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef", + "certificate_sha256": "b413e68c0bd2311c491ebd087ac5791c7c2ad8267793f92f6055d5521bd3ef4d", + "protocol_major": 1, + "minimum_android_api": 26, + "target_android_api": 37, + "build_tools": "36.0.0", + "toolchain": { + "kotlin": "2.4.20", + "agp": "9.3.1", + "compose": "1.12.1", + "coroutines": "1.10.2", + "serialization": "1.9.0" + }, + "sources": [ + { + "path": "androidApp/build.gradle.kts", + "sha256": "7bfbd5d27819700243b7ab1e318eec91024dc170059a92d587400608ec8a59b5" + }, + { + "path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt", + "sha256": "cd0d132657280d1bd9640916acb3d4844cea7ebfe7b1955988e8f5a0a0d10ae9" + }, + { + "path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/BleIntegrationTest.kt", + "sha256": "8dc7a28279e99e46d15c914955d8974d619c4e48a2fcab887f180b15b4b183ab" + }, + { + "path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/LayoutTest.kt", + "sha256": "2323f5e68b0f3d4771435628db7f880c6a8cefb27b5ea8d6c3401f3828b0e1b8" + }, + { + "path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/WifiSwitchTest.kt", + "sha256": "5ce335ff84fee01230733971964fefde3fcc6103172d3c40fee64f50838b7bcf" + }, + { + "path": "androidApp/src/main/AndroidManifest.xml", + "sha256": "186ed22b19731ea5bf3567e40d6487b95f1bced3dd2fce605de6557cb7c5763e" + }, + { + "path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/AndroidBle.kt", + "sha256": "a1af9d6593c3be1af0f2a39a3869eae2e1bbbb1dab8b775e06da5dcaa739e9ff" + }, + { + "path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/MainActivity.kt", + "sha256": "7bece662b79093a19fcdf34ad344cf42a57b60291f0536823395ae06663f4f23" + }, + { + "path": "build.gradle.kts", + "sha256": "feb3b6e64b846f0408cc40e95ae4b632ce9288a56bd6151bc3bbcc46f0549b49" + }, + { + "path": "gradle/libs.versions.toml", + "sha256": "ed393bb2b4d81a162f6b3d21968eeca113e23c2f5d5d22c3be9d104bc592a633" + }, + { + "path": "gradle/wrapper/gradle-wrapper.jar", + "sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" + }, + { + "path": "gradle/wrapper/gradle-wrapper.properties", + "sha256": "2cb0f9ddf992a26fc4947c91019136bff5e1ccfbf93bd905402b1abf69d15bc5" + }, + { + "path": "gradle.properties", + "sha256": "e7d684b7795fd04ab2e7e96038fa2bf4f9fd633578aa3bb2f5fe367b4f5ec159" + }, + { + "path": "gradlew", + "sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a" + }, + { + "path": "gradlew.bat", + "sha256": "475c4f08cd57cf2faa819e7f36d72aa93f0ad646ea23a8f7fa3ef54dee1cbc52" + }, + { + "path": "README.md", + "sha256": "7232ebbac0899152e312429fecaf6b3e14b31265b0c50a95c99a6213aa04b2cd" + }, + { + "path": "settings.gradle.kts", + "sha256": "3d53a77b5b92800475897fb55f910a7cfe008e2084e0b7185ebb5ce38e22cf53" + }, + { + "path": "sharedCore/build.gradle.kts", + "sha256": "9a420e39e3c655f676572d4486e415dbc8b4fd45904db6ad7b64d7fcd71f084b" + }, + { + "path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt", + "sha256": "48e7bd52250d541088ca2756a8a1dbd37aeedab1ffc03338c9bb7c6ed9d39d7d" + }, + { + "path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/DeviceLink.kt", + "sha256": "870345382351bbf6d37f4332c0fab099cb47b7b703428a98567a0086c3316be6" + }, + { + "path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Protocol.kt", + "sha256": "99264aff225076ef1b423801b9bba8797e0e06a9e759339bc52e896620e4b694" + }, + { + "path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/SecureChannel.kt", + "sha256": "bde40388bb74c9d47a4e099a14a0baeca3b6bda1539023dbd7dca831c6f24039" + }, + { + "path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt", + "sha256": "9cfd5291d5442ba79802e4193124bc2f22f05cd9a577001d33913d56b140a4a6" + }, + { + "path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ProtocolTest.kt", + "sha256": "5d26d3a50c5887d72dc91b2081273fbe4a5a968f01451ef75ab78903d5d42217" + }, + { + "path": "sharedCore/src/javaCryptoMain/kotlin/org/qimiaoscreen/core/JavaSecureChannel.kt", + "sha256": "554d663ceb6ec1be095b31e9ff9549af3ecd02f975059c22ce566c87225b05eb" + }, + { + "path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/CryptoInteropTest.kt", + "sha256": "2ddb06c4476952a69bf01c0a3a7164625ae8c0bfe9194349450a2e254888d4b3" + }, + { + "path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/RpcTimeoutTest.kt", + "sha256": "fca298fb2266ee7e68e6df76d837f7f191e0046aa5ba321a304c4b4369e064a6" + }, + { + "path": "sharedCore/src/jvmTest/resources/protocol-v1.json", + "sha256": "6280953b95a3f5d1ae531d7feaa6a62848ffa3e438e58308d2a27f04723803dc" + }, + { + "path": "sharedUi/build.gradle.kts", + "sha256": "d469b330d248647e6e93f98d69eb46730adab329ebaf71cbfd896d5426fd0ff1" + }, + { + "path": "sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt", + "sha256": "884e61008e49fd3f1c62969cbe38a798f896734fc70f5219b4e00bcb34bfe9a3" + } + ] +} diff --git a/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/奇妙小屏幕-0.2.1-debug.apk b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/奇妙小屏幕-0.2.1-debug.apk new file mode 100644 index 0000000..423da50 Binary files /dev/null and b/移动端相关内容/安卓app/构建与发布/交付包/0.2.1-4/奇妙小屏幕-0.2.1-debug.apk differ diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/README.md b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/README.md new file mode 100644 index 0000000..e704349 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/README.md @@ -0,0 +1,43 @@ +# Android 0.2.1 界面细节优化验收 + +App **0.2.1 / versionCode 4**,debug 签名,协议主版本1。测试代号 android-test-a(Android 13)。设备软件1.1.3,功能时间2026-09-27T17:51+08:00;本轮没有设备部署、驱动修改、OTA或IMG导出,也没有改设备版本或功能时间。 + +## 实现 + +顶栏长文字以30 dp/秒无限循环、每轮间隔1秒,短文字静止,内容变化从头开始,菜单固定。认证成功后的扫描条目改为断开,重新扫描保留当前连接条目;旧扫描信号不冒充实时RSSI,实时结果缺失显示暂不可用。两页设备画面、标题、等待与过期提示居中,刷新按钮仍在右侧且标题预留对称空间。 + +## 最终结果 + +- 隔离构建、JVM **15项**、Android lint通过;lint有16条警告(依赖版本、图标、现有API用法等),无错误。登记门禁unittest **8项**通过。 +- root真机布局三组全部通过:约340dp、1.0倍字体竖屏 **77.282秒**;1.3倍字体竖屏 **54.004秒**;1.3倍字体横屏 **84.774秒**。每组覆盖四栏和菜单、连接中重新扫描保留、条目实际断开及手动再连、两页画面和标题中心差不超过3像素、刷新按钮右侧且不重叠。输入仅使用实时语义位置与root。密度、字体、方向最终恢复并回读通过。 +- 实际Activity **87.006秒**通过:后台30秒释放及返回重连、菜单、设备昵称修改后重新连接、恢复原昵称、忘记后再次手动连接。阶段最后主动断开。 +- 真实BLE只读回归 **127.671秒**通过:两次加密会话、状态/设置/存储/逻辑帧/WiFi扫描、内容列表顺序摘要、配置冲突后会话保持、Controller页面读取、后台释放和前台重连。本轮指定3轮轮询,不等同历史60轮耐久复测;未实际切网、重应用亮度或改变显示内容。 +- debug签名和逐文件源码摘要核对通过;安装到登记手机的base.apk SHA-256及版本与交付包一致。证据见installed-artifact.json。 + +顶栏完整语义文本和菜单边界经真机核验;无限循环参数及内容变化重启经源码/编译核对,语义检查不代表实际像素运动的目视验收。未截图、未启动摄像头;实屏视觉检查不适用。 + +## 失败、修正及恢复 + +首次两组竖屏通过,横屏找不到扫描名称,见首次布局尝试.md。第二次遇到StaleObjectException(27.65秒),见layout-root-attempt-2.log。第三次第一组通过,第二组手动重连后未找到连接条目(70.945秒);第四次同阶段未找到条目(74.747秒),分别保留在attempt-3/4日志。 + +测试补充列表可见区域内的root滚动、失效语义节点重新获取、条目容器内可见且启用的按钮筛选、非空边界检查,以及两次间隔400毫秒的相同边界确认后只点击一次。新增阶段与脱敏错误分类用于区分输入、连接和最终状态。随后最终三组全部通过,没有放宽原验收断言,没有用普通注入或无障碍动作替代root。此前重连条目未出现的唯一根因未证实,不把重测通过作为根因证明。 + +可复用方法已补入 ../../如何安卓测试/常见问题与排障.md 第12节及测试README;单次证据保留在本目录。所有布局运行的finally执行恢复,最后运行另回读密度、字体、方向核验通过。 + +## 交付与边界 + +交付包:../../构建与发布/交付包/0.2.1-4/。构建日期 **2026-09-27 20:28 +08:00**。 + +APK SHA-256:`0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef`。 + +源码树摘要:`671d5d7b9de3c808c3f217ee15e2780d85d3d8dffa5333437d1bf1c79eab8423`(按manifest.sources顺序,将相对路径、空格、摘要和换行连接后计算);逐文件摘要及证书摘要见交付manifest,测试APK摘要见artifact-summary.json。 + +旧Android版本、其他机型、真实多设备切换本轮未实测;共享逻辑覆盖设备切换、异常、身份失败及实时信号等待。沿用既有未验证边界,不声称本轮完成所有首版功能或网络专项。日志已脱敏,未记录真实凭据、手机标识或开发机绝对路径。 + +## 清理与仓库检查 + +手机短时root辅助文件由运行器finally清理;布局设置已恢复并核验,测试阶段正常主动断开。交付包与脱敏证据长期保留。 + +本机四个隔离构建目录的批量递归清理被自动安全审核拒绝;随后核对明确绝对路径及目录属性,以单个明确LiteralPath请求清理,仍被拒绝,返回理由仅为“blocked by policy”。未改用其他工具绕过拒绝,目录保留在受保护临时目录内:`qms-ui-021/qms-build-s_9wvlf3`、`qms-ui-021-final/qms-build-wy8k36kg`、`qms-ui-021-verified/qms-build-o9n44f_y`、`qms-ui-021-delivery/qms-build-vdbzxfp4`。这些目录不进入Git、不被后续源码或构建依赖;功能验收及交付不受影响。 + +最终仓库卫生检查与git diff --check通过,没有输出真实秘密。 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/activity-root.log b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/activity-root.log new file mode 100644 index 0000000..80541cd --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/activity-root.log @@ -0,0 +1,35 @@ +私有凭据文件可用;未输出任何凭据值。 +Performing Streamed Install +Success + +Performing Streamed Install +Success + + + + +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.AppLifecycleTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.AppLifecycleTest: +INSTRUMENTATION_STATUS: test=actualActivityReturnsFromBackground +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.AppLifecycleTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: test=actualActivityReturnsFromBackground +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= + +Time: 87.006 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 + +Runner exit code: 0 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/artifact-summary.json b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/artifact-summary.json new file mode 100644 index 0000000..ba6ff2d --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/artifact-summary.json @@ -0,0 +1,29 @@ +{ + "app_version": "0.2.1", + "version_code": 4, + "protocol_major": 1, + "source_tree_sha256": "671d5d7b9de3c808c3f217ee15e2780d85d3d8dffa5333437d1bf1c79eab8423", + "apk_sha256": "0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef", + "build_date": "2026-09-27 20:28 +08:00", + "layout": { + "passed_runs": 3, + "seconds": [ + "77.282", + "54.004", + "84.774" + ] + }, + "activity": { + "passed_runs": 1, + "seconds": [ + "87.006" + ] + }, + "ble": { + "passed_runs": 1, + "seconds": [ + "127.671" + ] + }, + "test_apk_sha256": "2ad184a000db4ef9e9d7add0cfe913a455f054f4f9b39b34b0c3b36783569a92" +} diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/ble-root.log b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/ble-root.log new file mode 100644 index 0000000..e088061 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/ble-root.log @@ -0,0 +1,275 @@ +私有凭据文件可用;未输出任何凭据值。 +Performing Streamed Install +Success + +Performing Streamed Install +Success + + + + +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.BleIntegrationTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.BleIntegrationTest: +INSTRUMENTATION_STATUS: test=encryptedReadOnlySessionAndReconnect +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: stream=BLE stage: scan complete + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: GATT ready 0 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=1 index=0 count=1 bytes=215 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=2 count=50 bytes=20 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=134 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=2 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: encrypted session open + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=84 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=21 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: status received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=86 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: settings received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=85 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=1 bytes=232 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: storage received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=86 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=16 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: library received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: library persistent order verified + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=83 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: frame received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=82 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=4 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=86 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=1 bytes=84 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=83 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: WiFi scan completed without logging network names + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=171 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=1 bytes=148 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=87 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=1 bytes=85 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: configuration conflict preserves session + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=85 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=21 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=84 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: continuous polling cycle 0 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=85 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=22 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=84 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=85 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=22 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=84 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=5 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: intentional session close 0 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: GATT ready 1 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=1 index=0 count=1 bytes=215 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=2 count=50 bytes=20 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=134 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=2 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: encrypted session open + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=84 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=22 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: status received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=86 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: settings received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=85 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=1 bytes=232 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: storage received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=86 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=16 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: library received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: library persistent order verified + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=83 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=3 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: frame received + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=82 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=4 bytes=244 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: send kind=3 index=0 count=1 bytes=86 mtu=247 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: receive kind=3 count=1 bytes=84 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: intentional session close 1 + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: controller device page ready + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: controller content and settings ready + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: controller background released + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=BLE stage: controller foreground reconnected + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.BleIntegrationTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: test=encryptedReadOnlySessionAndReconnect +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= + +Time: 127.671 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 + +Runner exit code: 0 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/device-metadata.json b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/device-metadata.json new file mode 100644 index 0000000..bb8bbb7 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/device-metadata.json @@ -0,0 +1,5 @@ +{ + "VERSION": "1.1.3", + "FEATURE_UPDATED_AT": "2026-09-27T17:51+08:00", + "service_active": true +} diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/installed-artifact.json b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/installed-artifact.json new file mode 100644 index 0000000..5d38609 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/installed-artifact.json @@ -0,0 +1,6 @@ +{ + "alias": "android-test-a", + "apk_sha256": "0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef", + "installed_apk_matches": true, + "installed_version_matches": true +} diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/jvm-summary.json b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/jvm-summary.json new file mode 100644 index 0000000..3525006 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/jvm-summary.json @@ -0,0 +1,30 @@ +[ + { + "name": "ControllerRecoveryTest[jvm]", + "tests": "9", + "failures": "0", + "errors": "0", + "time": "0.304" + }, + { + "name": "CryptoInteropTest[jvm]", + "tests": "3", + "failures": "0", + "errors": "0", + "time": "0.074" + }, + { + "name": "ProtocolTest[jvm]", + "tests": "2", + "failures": "0", + "errors": "0", + "time": "0.002" + }, + { + "name": "RpcTimeoutTest[jvm]", + "tests": "1", + "failures": "0", + "errors": "0", + "time": "0.033" + } +] diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-2.log b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-2.log new file mode 100644 index 0000000..7dabf1d --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-2.log @@ -0,0 +1,67 @@ +私有凭据文件可用;未输出任何凭据值。 +Performing Streamed Install +Success + +Performing Streamed Install +Success + + + + +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stack=androidx.test.uiautomator.StaleObjectException + at androidx.test.uiautomator.UiObject2.getAccessibilityNodeInfo(UiObject2.java:1018) + at androidx.test.uiautomator.UiObject2.getVisibleBounds(UiObject2.java:310) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$visible(LayoutTest.kt:32) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:61) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:88) + +INSTRUMENTATION_STATUS: stream= +Error in narrowLargeFontAndLandscape(org.qimiaoscreen.controller.LayoutTest): +androidx.test.uiautomator.StaleObjectException + at androidx.test.uiautomator.UiObject2.getAccessibilityNodeInfo(UiObject2.java:1018) + at androidx.test.uiautomator.UiObject2.getVisibleBounds(UiObject2.java:310) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$visible(LayoutTest.kt:32) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:61) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:88) + +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: -2 +INSTRUMENTATION_RESULT: stream= + +Time: 27.65 +There was 1 failure: +1) narrowLargeFontAndLandscape(org.qimiaoscreen.controller.LayoutTest) +androidx.test.uiautomator.StaleObjectException + at androidx.test.uiautomator.UiObject2.getAccessibilityNodeInfo(UiObject2.java:1018) + at androidx.test.uiautomator.UiObject2.getVisibleBounds(UiObject2.java:310) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$visible(LayoutTest.kt:32) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:61) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:88) + +FAILURES!!! +Tests run: 1, Failures: 1 + + +INSTRUMENTATION_CODE: -1 + +Layout settings restored and verified +Traceback (most recent call last): + File "<仓库根目录>\移动端相关内容\安卓app\如何安卓测试\run_ble_test.py", line 154, in + main() + ~~~~^^ + File "<仓库根目录>\移动端相关内容\安卓app\如何安卓测试\run_ble_test.py", line 129, in main + raise RuntimeError("Instrumentation did not pass") +RuntimeError: Instrumentation did not pass +Runner exit code: 1 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-3.log b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-3.log new file mode 100644 index 0000000..74351af --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-3.log @@ -0,0 +1,86 @@ +私有凭据文件可用;未输出任何凭据值。 +Performing Streamed Install +Success + +Performing Streamed Install +Success + + + + +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= + +Time: 47.366 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 + +Layout passed: font=1.0, rotation=0 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stack=java.lang.IllegalStateException: Required App semantic tag unavailable: connected_scan_disconnect + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:67) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$connectScanned(LayoutTest.kt:80) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:111) + +INSTRUMENTATION_STATUS: stream= +Error in narrowLargeFontAndLandscape(org.qimiaoscreen.controller.LayoutTest): +java.lang.IllegalStateException: Required App semantic tag unavailable: connected_scan_disconnect + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:67) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$connectScanned(LayoutTest.kt:80) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:111) + +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: -2 +INSTRUMENTATION_RESULT: stream= + +Time: 70.945 +There was 1 failure: +1) narrowLargeFontAndLandscape(org.qimiaoscreen.controller.LayoutTest) +java.lang.IllegalStateException: Required App semantic tag unavailable: connected_scan_disconnect + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:67) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$connectScanned(LayoutTest.kt:80) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:111) + +FAILURES!!! +Tests run: 1, Failures: 1 + + +INSTRUMENTATION_CODE: -1 + +Layout settings restored and verified +Traceback (most recent call last): + File "<仓库根目录>\移动端相关内容\安卓app\如何安卓测试\run_ble_test.py", line 154, in + main() + ~~~~^^ + File "<仓库根目录>\移动端相关内容\安卓app\如何安卓测试\run_ble_test.py", line 129, in main + raise RuntimeError("Instrumentation did not pass") +RuntimeError: Instrumentation did not pass +Runner exit code: 1 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-4.log b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-4.log new file mode 100644 index 0000000..2d1d5f7 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root-attempt-4.log @@ -0,0 +1,79 @@ +私有凭据文件可用;未输出任何凭据值。 +Performing Streamed Install +Success + +Performing Streamed Install +Success + + + + +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: stream=Layout stage: initial-connect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: scan-while-connected + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-status + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-content + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: disconnect-row + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: manual-reconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stack=java.lang.IllegalStateException: Required App semantic tag unavailable: connected_scan_disconnect + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:68) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$connectScanned(LayoutTest.kt:85) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:119) + +INSTRUMENTATION_STATUS: stream= +Error in narrowLargeFontAndLandscape(org.qimiaoscreen.controller.LayoutTest): +java.lang.IllegalStateException: Required App semantic tag unavailable: connected_scan_disconnect + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:68) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$connectScanned(LayoutTest.kt:85) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:119) + +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: -2 +INSTRUMENTATION_RESULT: stream= + +Time: 74.747 +There was 1 failure: +1) narrowLargeFontAndLandscape(org.qimiaoscreen.controller.LayoutTest) +java.lang.IllegalStateException: Required App semantic tag unavailable: connected_scan_disconnect + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$tagged(LayoutTest.kt:68) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape$connectScanned(LayoutTest.kt:85) + at org.qimiaoscreen.controller.LayoutTest.narrowLargeFontAndLandscape(LayoutTest.kt:119) + +FAILURES!!! +Tests run: 1, Failures: 1 + + +INSTRUMENTATION_CODE: -1 + +Layout settings restored and verified +Traceback (most recent call last): + File "<仓库根目录>\移动端相关内容\安卓app\如何安卓测试\run_ble_test.py", line 154, in + main() + ~~~~^^ + File "<仓库根目录>\移动端相关内容\安卓app\如何安卓测试\run_ble_test.py", line 129, in main + raise RuntimeError("Instrumentation did not pass") +RuntimeError: Instrumentation did not pass +Runner exit code: 1 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root.log b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root.log new file mode 100644 index 0000000..d0644a6 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/layout-root.log @@ -0,0 +1,159 @@ +私有凭据文件可用;未输出任何凭据值。 +Performing Streamed Install +Success + +Performing Streamed Install +Success + + + + +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: stream=Layout stage: initial-connect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: scan-while-connected + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-status + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-content + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: disconnect-row + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: manual-reconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: connect-request-observed=false + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: final-disconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= + +Time: 77.282 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 + +Layout passed: font=1.0, rotation=0 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: stream=Layout stage: initial-connect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: scan-while-connected + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-status + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-content + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: disconnect-row + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: manual-reconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: connect-request-observed=false + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: final-disconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= + +Time: 54.004 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 + +Layout passed: font=1.3, rotation=0 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream= +org.qimiaoscreen.controller.LayoutTest: +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 1 +INSTRUMENTATION_STATUS: stream=Layout stage: initial-connect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: scan-while-connected + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-status + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: frame-content + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: disconnect-row + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: manual-reconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: connect-request-observed=false + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: stream=Layout stage: final-disconnect + +INSTRUMENTATION_STATUS_CODE: 2 +INSTRUMENTATION_STATUS: class=org.qimiaoscreen.controller.LayoutTest +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: id=AndroidJUnitRunner +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: test=narrowLargeFontAndLandscape +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= + +Time: 84.774 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 + +Layout passed: font=1.3, rotation=1 +Layout settings restored and verified +Runner exit code: 0 diff --git a/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/首次布局尝试.md b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/首次布局尝试.md new file mode 100644 index 0000000..8d4e343 --- /dev/null +++ b/移动端相关内容/安卓app/测试结果归档/2026-09-27_界面细节优化/首次布局尝试.md @@ -0,0 +1,5 @@ +# 首次布局尝试 + +App 0.2.1 / versionCode 4。约340dp、1.0倍字体竖屏38.726秒通过;1.3倍字体竖屏49.475秒通过。1.3倍字体横屏64.751秒失败:连接辅助函数在20秒等待内未找到扫描目标名称。当时未执行列表滚动,不能仅凭该失败区分条目不可见与未扫描到目标,也未认定蓝牙根因。 + +随后给连接列表增加稳定语义标签,测试按该列表实时可见边界执行有界root滑动定位;再次隔离构建后重测,结果见 layout-root.log。首次失败保留。首次运行 finally 已执行布局恢复;后续运行器增加密度回读,与既有字体和方向回读一并核验。 diff --git a/移动端相关内容/安卓app/测试结果归档/实施状态.md b/移动端相关内容/安卓app/测试结果归档/实施状态.md index 624c18a..29ca6d1 100644 --- a/移动端相关内容/安卓app/测试结果归档/实施状态.md +++ b/移动端相关内容/安卓app/测试结果归档/实施状态.md @@ -1,5 +1,9 @@ # 当前实施状态 +更新时间:2026-09-27。当前App为 **0.2.1 / versionCode 4**,顶栏循环、扫描条目断开/连接保留、两页预览居中已实现。交付包 `../构建与发布/交付包/0.2.1-4/`;本轮证据及未验证边界见 [界面细节优化验收](2026-09-27_界面细节优化/README.md)。JVM15项、root三组布局、实际Activity和BLE只读回归通过,最终安装摘要一致;早期测试失败与修正保留。测试流程和排障第12节已更新。设备协议、服务和驱动不变。 + +以下为历史验收记录,不将历史通过项自动归为本轮重测: + 更新时间:2026-09-26。当前 App 为 **0.2.0 / versionCode 3**,四栏界面优化已实现。交付包:`../构建与发布/交付包/0.2.0-3/`,本轮证据与边界见 `2026-09-26_四栏界面优化.md`。设备功能已部署,协议主版本仍为 1;不导出 OTA/IMG、不推进设备发布版本。 已通过:独立构建、JVM 13 项、设备相关回归 46 项、root 真机四栏/菜单/设备重命名与记忆/后台连接生命周期、窄屏大字体横屏导航、真实 systemd 生命周期。完整 BLE(301.514 秒)、错误密码明确分类及原网络恢复、最终覆盖安装摘要核对均通过,详见本轮报告;真实多设备、旧 Android、跨 SSID 与实际 DHCP/静态互换不能按模拟或表单测试记为通过。 diff --git a/移动端相关内容/开发要求/移动端需求.md b/移动端相关内容/开发要求/移动端需求.md index d3ea8a9..c511b2c 100644 --- a/移动端相关内容/开发要求/移动端需求.md +++ b/移动端相关内容/开发要求/移动端需求.md @@ -66,3 +66,9 @@ MOBILE-RECONNECT/MOBILE-ERRORS 补充:断线清除旧画面、状态、缩 - MOBILE-WIFI-SECRET:不回读原密码,“已设置密码”占位,原 SSID 留空保留,替换密码只驻留内存;取消确认不提交。 - MOBILE-APP-SETTINGS:顶部三点菜单关于/软件设置,关于显示版本、构建编号及北京时间打包日期;手机昵称可离线修改并同步当前会话。 - MOBILE-TEST-PRIVACY:本 App 调试所需测试权限可通过登记手机 ADB root 直接获取,无需重复询问;所有自动点击使用 root 并依据实时语义位置,禁止无障碍绕过。特殊情况或权限不足必须停止等待用户。视觉和物理门禁不变。 + +## 2026-09-27 界面修订 + +- MOBILE-STATUS:顶栏长文字自动向左无限循环(30 dp/秒、每轮间隔1秒),短文字静止;菜单固定,完整语义文字保留。 +- MOBILE-DISCOVERY/CONNECTION:握手成功后扫描条目显示已连接及断开按钮;再次扫描保留当前连接条目,旧扫描信号不作实时信号。断开、失败和异常清理会话关联;其他条目仍可连接,连接期间禁止重复操作。 +- MOBILE-PREVIEW:两处设备画面、标题、等待与过期提示按整个卡片居中,刷新按钮固定右侧;标题预留对称空间,放大字体不与按钮重叠。 diff --git a/移动端相关内容/开发要求/跨端兼容与变更记录.md b/移动端相关内容/开发要求/跨端兼容与变更记录.md index ca9c236..c4171ed 100644 --- a/移动端相关内容/开发要求/跨端兼容与变更记录.md +++ b/移动端相关内容/开发要求/跨端兼容与变更记录.md @@ -54,3 +54,7 @@ Android 断线清理与旧扫描回调隔离已通过模拟及 Activity 真机 ### 2026-09-26 四栏优化 用户明确要求 App 迭代。App 0.2.0 / code 3:四栏、设备记忆、中文 WiFi 设置、关于与本地设置;旧记录通过后续握手迁移。设备 library.list 对齐持久顺序,task.get 增加可选 error_code;协议主版本 1,旧 App 忽略新字段。无驱动修改,无新板端依赖,不导出 OTA/IMG。实际验证结果另行归档。 + +## 2026-09-27 Android 0.2.1 界面优化 + +MOBILE-STATUS/DISCOVERY/PREVIEW:顶栏自动循环、扫描条目断开与连接保留、设备画面居中。仅修改手机界面及会话显示状态;协议1.0、设备接口、广播仲裁和驱动不变,不要求设备升级。不推进设备发布版本或 FEATURE_UPDATED_AT。