Improve Android connection and preview UI testing

This commit is contained in:
2026-09-27 20:52:24 +08:00
parent f80c4d9938
commit 55374356ed
32 changed files with 1227 additions and 31 deletions
@@ -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"
}
@@ -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)))
@@ -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) }
}
}
}
@@ -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 ?: "连接失败") }
}
}
}
@@ -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() }
}
@@ -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"
}
]
}
@@ -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/静态互换不能按模拟或表单测试记为通过。