diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt b/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt index 8e95f58e..ea574604 100644 --- a/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt +++ b/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt @@ -66,14 +66,15 @@ object Build { object VERSION { var SDK_INT = 36 }; object VERSION_CODES { const object Process { const val THREAD_PRIORITY_AUDIO = -16; fun setThreadPriority(value: Int) {} } class Bundle { val values = mutableMapOf() + var failReads = false fun putInt(key: String, value: Int) { values[key] = value } fun putLong(key: String, value: Long) { values[key] = value } fun putString(key: String, value: String) { values[key] = value } fun putBundle(key: String, value: Bundle) { values[key] = value } fun putParcelable(key: String, value: Any) { values[key] = value } - fun getLong(key: String, fallback: Long): Long = values[key] as? Long ?: fallback - fun getString(key: String): String? = values[key] as? String - fun keySet() = values.keys + fun getLong(key: String, fallback: Long): Long { check(!failReads) { "Bundle decode failed" }; return values[key] as? Long ?: fallback } + fun getString(key: String): String? { check(!failReads) { "Bundle decode failed" }; return values[key] as? String } + fun keySet(): Set { check(!failReads) { "Bundle decode failed" }; return values.keys } } data class ParcelUuid(val uuid: java.util.UUID) interface OutcomeReceiver { fun onResult(result: R?); fun onError(error: E) } diff --git a/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/TelecomLifecycleTest.kt b/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/TelecomLifecycleTest.kt index 35639a7e..8f87052e 100644 --- a/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/TelecomLifecycleTest.kt +++ b/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/TelecomLifecycleTest.kt @@ -141,4 +141,50 @@ class TelecomLifecycleTest { conn.callAudioState = CallAudioState(CallAudioState.ROUTE_WIRED_HEADSET, 12); conn.onCallAudioStateChanged(conn.callAudioState) assertEquals(AudioRoute.HEADSET, conn.confirmedRoute) } + + @Test fun failedEndpointRequestDoesNotSendAnUnneededRestoreToTheUnchangedRoute() { + val conn = routed(); conn.routeTo(AudioRoute.SPEAKER); conn.routeTo(AudioRoute.EARPIECE) + conn.endpointRequests.single().fail(); main.advanceBy(0) + assertEquals(1, conn.endpointRequests.size); assertEquals(AudioRoute.EARPIECE, conn.confirmedRoute) + } + @Test fun endpointConfirmationBeforeItsOutcomePreservesTheLatestDesiredRoute() { + val conn = routed(); conn.routeTo(AudioRoute.SPEAKER); conn.routeTo(AudioRoute.EARPIECE) + conn.onCallEndpointChanged(speaker); conn.endpointRequests.first().succeed(); main.advanceBy(0) + assertEquals(2, conn.endpointRequests.size); assertEquals(earpiece, conn.endpointRequests.last().endpoint) + } + @Test fun outcomeFromAThrowingSdkRequestCannotClearANewerEndpointRequest() { + val conn = routed(); conn.failures = setOf("endpoint after"); conn.routeTo(AudioRoute.SPEAKER) + conn.failures = emptySet(); conn.routeTo(AudioRoute.EARPIECE); assertEquals(2, conn.endpointRequests.size) + conn.endpointRequests.first().succeed(); main.advanceBy(0) + val pending = UtunConnection::class.java.getDeclaredField("requestedEndpoint").apply { isAccessible = true }.get(conn) + assertEquals(earpiece.identifier, pending) + } + @Test fun routeWaitsForAMissingEndpointAndUsesItWhenTheListChanges() { + val conn = registered(); conn.onAvailableCallEndpointsChanged(mutableListOf(earpiece)); conn.routeTo(AudioRoute.SPEAKER) + assertTrue(conn.endpointRequests.isEmpty()) + conn.onAvailableCallEndpointsChanged(mutableListOf(earpiece, speaker)); assertEquals(1, conn.endpointRequests.size) + } + @Test fun malformedServiceIdentityCancelsTheConnectionAndReportsItsKnownCallId() { + current(); val extras = Bundle().apply { putLong(TelecomCallManager.EXTRA_CALL_ID, 17) } + val result = UtunConnectionService().onCreateIncomingConnection(null, ConnectionRequest(extras)) + main.advanceBy(0); assertTrue(result!!.cancelled); assertTrue("failed:17" in events) + } + @Test fun undecodableExtrasCannotEscapeFromConnectionServiceCallbacks() { + val request = ConnectionRequest(Bundle().apply { failReads = true }); val service = UtunConnectionService() + val result = service.onCreateOutgoingConnection(null, request) + service.onCreateIncomingConnectionFailed(null, request); service.onCreateOutgoingConnectionFailed(null, request) + main.advanceBy(0); assertTrue(result!!.cancelled); assertEquals(listOf("failed:0", "failed:0", "failed:0"), events) + } + @Test fun constructorFailureReturnsAFailedSdkConnectionAndNotifiesTheController() { + current(); Connection.nextFailures = setOf("properties") + val extras = Bundle().apply { putLong(TelecomCallManager.EXTRA_CALL_ID, 17); putLong(TelecomCallManager.EXTRA_PEER_NODE_ID, 99) } + val result = UtunConnectionService().onCreateOutgoingConnection(null, ConnectionRequest(extras)) + main.advanceBy(0); assertEquals(android.telecom.DisconnectCause.ERROR, result!!.disconnectCause!!.code) + assertTrue("failed:17" in events) + } + @Test fun registryMutationsOutsideMainFailBeforeTouchingItsOwner() { + val conn = registered(); val error = java.util.concurrent.atomic.AtomicReference() + Thread { try { TelecomCallManager.removeConnection(17) } catch (e: Throwable) { error.set(e) } }.apply { start(); join() } + assertTrue(error.get() is IllegalStateException); assertSame(conn, TelecomCallManager.connectionFor(17)); assertFalse(conn.destroyed) + } }