Improve Android connection and preview UI testing
This commit is contained in:
@@ -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 真机布局验收。设备接口、广播仲裁与驱动不变,不需要设备部署或实屏视觉检查。
|
||||
|
||||
@@ -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停启步骤。
|
||||
|
||||
@@ -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 滑动定位扫描条目,按当前条目容器内的资源标签选连接/断开,不能假设设备名称始终在首屏。布局恢复须同时回读密度覆盖、字体与旋转;运行器输出恢复核验结果。
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -61,3 +61,11 @@ Compose 的禁用语义可能位于父节点,文字节点自身仍 enabled。
|
||||
每轮检查:本轮有哪些失败/调整、是否已有可重复防范步骤、恢复是否核验、哪些原因仍未知。已验证解法更新本页和对应流程/构建说明;一次性证据留归档,不能将未证实推测写成固定根因。
|
||||
|
||||
最终反馈写明经验已更新的文档,以及尚未解决的问题或未验证边界。若没有新增经验,说明沿用现有方法即可,不复制重复条目。遵守根协作规则:不写真实凭据、手机标识、开发机绝对路径,文档更新后执行仓库卫生检查。
|
||||
|
||||
## 12. 轮询或滑动后节点失效、名称可见但按钮无法点击
|
||||
|
||||
本轮三种布局已验证:读取语义节点期间可能出现 StaleObjectException,应在原有有界期限内丢弃旧节点并重新查找,不复用旧坐标、不放宽断言。滚动定位使用 connection_list 的实时可见边界;扫描按钮还要限制在目标设备条目容器,检查按钮启用且 visibleBounds 非空。名称露出不证明兄弟按钮可见,空边界不能取中心点点击。
|
||||
|
||||
滑动后通过两次间隔400毫秒的实时边界相等检查确认目标停稳(最多5秒),然后仅发送一次 root 点击;不因未看到中间状态而重复点击连接。记录初次连接、连接中扫描、两处预览、条目断开、手动重连等阶段。短暂“连接中”未被观测不代表未发起请求,终态以认证连接和条目状态核验为准。
|
||||
|
||||
本轮失败、恢复与最终结果见 `../测试结果归档/2026-09-27_界面细节优化/`;重测通过不证明此前连接未出现的唯一根因。字体、旋转和密度恢复后均回读核验。
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)))
|
||||
|
||||
+118
-8
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -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<DiscoveredDevice> = 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<JsonObject> = emptyList(), val networks: List<JsonObject> = 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 ?: "连接失败") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-2
@@ -12,11 +12,12 @@ private class SimulatedPlatform : MobilePlatform {
|
||||
val preferences = mutableMapOf<String, String>()
|
||||
val links = mutableListOf<SimulatedLink>()
|
||||
val scanCallbacks = mutableListOf<(DiscoveredDevice) -> Unit>()
|
||||
var signal: Deferred<Int?>? = 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<Int?>?) : DeviceLink {
|
||||
override val mtu = 247
|
||||
private val receiver = Reassembler()
|
||||
private val messages = Channel<ByteArray>(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<Int?>()
|
||||
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() }
|
||||
}
|
||||
|
||||
|
||||
+35
-15
@@ -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<RememberedDevice?>(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)
|
||||
} }
|
||||
}
|
||||
|
||||
|
||||
@@ -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_界面细节优化。
|
||||
|
||||
@@ -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签名。
|
||||
@@ -0,0 +1 @@
|
||||
0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef 奇妙小屏幕-0.2.1-debug.apk
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -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通过,没有输出真实秘密。
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"VERSION": "1.1.3",
|
||||
"FEATURE_UPDATED_AT": "2026-09-27T17:51+08:00",
|
||||
"service_active": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"alias": "android-test-a",
|
||||
"apk_sha256": "0cd888b0645d6d45ce457d2f334c69bb79fe52af3a4748080158db164fe8b9ef",
|
||||
"installed_apk_matches": true,
|
||||
"installed_version_matches": true
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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 <module>
|
||||
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
|
||||
@@ -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 <module>
|
||||
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
|
||||
@@ -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 <module>
|
||||
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
|
||||
@@ -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
|
||||
@@ -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 已执行布局恢复;后续运行器增加密度回读,与既有字体和方向回读一并核验。
|
||||
@@ -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/静态互换不能按模拟或表单测试记为通过。
|
||||
|
||||
@@ -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:两处设备画面、标题、等待与过期提示按整个卡片居中,刷新按钮固定右侧;标题预留对称空间,放大字体不与按钮重叠。
|
||||
|
||||
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user