Improve Android connection and preview UI testing
This commit is contained in:
+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() }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user