Exercise real Telecom ownership, callbacks and endpoint routing
This commit is contained in:
1 parent
8a9e421ea7
commit
8d3cee4a56
1 file changed
+144
+144
@@ -0,0 +1,144 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.ParcelUuid
|
||||
import android.telecom.CallAudioState
|
||||
import android.telecom.CallEndpoint
|
||||
import android.telecom.Connection
|
||||
import android.telecom.ConnectionRequest
|
||||
import android.telecom.TelecomManager
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/** Настоящие manager/Connection/ConnectionService, управляемые SDK IPC и endpoint outcomes. */
|
||||
class TelecomLifecycleTest {
|
||||
private val owned = mutableListOf<Pair<Long, UtunConnection>>()
|
||||
private val events = mutableListOf<String>()
|
||||
private val main get() = Looper.getMainLooper()
|
||||
private val earpiece = endpoint(CallEndpoint.TYPE_EARPIECE)
|
||||
private val speaker = endpoint(CallEndpoint.TYPE_SPEAKER)
|
||||
private var failAvailable = false
|
||||
private val listener = object : TelecomCallManager.Listener {
|
||||
override fun onAnswer(callId: Long) { events.add("answer:$callId") }
|
||||
override fun onReject(callId: Long) { events.add("reject:$callId") }
|
||||
override fun onDisconnect(callId: Long) { events.add("disconnect:$callId") }
|
||||
override fun onIncomingUiAllowed(callId: Long) { events.add("ui:$callId") }
|
||||
override fun onConnectionFailed(callId: Long) { events.add("failed:$callId") }
|
||||
override fun onConnectionAvailable(callId: Long, connection: UtunConnection) {
|
||||
check(!failAvailable) { "listener unavailable" }; events.add("available:$callId")
|
||||
}
|
||||
}
|
||||
@Before fun prepare() {
|
||||
Build.VERSION.SDK_INT = 36; Handler.rejectImmediate = false; Handler.rejectDelayed = false
|
||||
TelecomManager.shared = TelecomManager(); TelecomManager.failLookup = false; Connection.nextFailures = emptySet(); Connection.created.clear()
|
||||
for (name in listOf("ready", "phoneAccountHandle")) TelecomCallManager::class.java.getDeclaredField(name)
|
||||
.apply { isAccessible = true }.set(TelecomCallManager, if (name == "ready") false else null)
|
||||
TelecomCallManager.setListener(listener); CallController.state.value = null; LogManager.entries.clear()
|
||||
}
|
||||
@After fun release() {
|
||||
Handler.rejectImmediate = false; Handler.rejectDelayed = false; Build.VERSION.SDK_INT = 36
|
||||
TelecomCallManager.setListener(null); Connection.created.forEach { it.failures = emptySet() }
|
||||
owned.forEach { TelecomCallManager.removeConnection(it.first) }; main.advanceBy(30_000)
|
||||
assertTrue(owned.all { it.second.destroyed })
|
||||
val pending = TelecomCallManager::class.java.getDeclaredField("closing").apply { isAccessible = true }.get(TelecomCallManager) as Map<*, *>
|
||||
assertTrue(pending.isEmpty()); CallController.state.value = null
|
||||
}
|
||||
private fun endpoint(type: Int) = CallEndpoint("endpoint $type", type, ParcelUuid(java.util.UUID.randomUUID()))
|
||||
private fun connection(id: Long = 17) = UtunConnection(id, 99, "peer").also { owned.add(id to it) }
|
||||
private fun current(id: Long = 17, phase: CallPhase = CallPhase.ACTIVE) { CallController.state.value = HostCallState(id, phase) }
|
||||
private fun registered(id: Long = 17): UtunConnection { current(id); return connection(id).also { assertTrue(TelecomCallManager.registerConnection(id, it)) } }
|
||||
private fun routed() = registered().also { it.onAvailableCallEndpointsChanged(mutableListOf(earpiece, speaker)); it.onCallEndpointChanged(earpiece) }
|
||||
|
||||
@Test fun accountLookupAndRegistrationFailuresRemainRetryable() {
|
||||
TelecomManager.failLookup = true; TelecomCallManager.init(); assertFalse(TelecomCallManager.isReady())
|
||||
TelecomManager.failLookup = false; TelecomManager.shared!!.failures = setOf("register after")
|
||||
TelecomCallManager.init(); assertFalse(TelecomCallManager.isReady())
|
||||
TelecomManager.shared!!.failures = emptySet(); TelecomCallManager.init(); assertTrue(TelecomCallManager.isReady())
|
||||
}
|
||||
@Test fun outgoingSdkExtrasReachConnectionServiceWithUnsignedIdsUnchanged() {
|
||||
TelecomCallManager.init(); val id = Long.MIN_VALUE + 7; current(id, CallPhase.OUTGOING)
|
||||
assertTrue(TelecomCallManager.placeCall(id, Long.MIN_VALUE, "peer"))
|
||||
val extras = TelecomManager.shared!!.outgoing.single().second.values[TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS] as Bundle
|
||||
val result = UtunConnectionService().onCreateOutgoingConnection(null, ConnectionRequest(extras))
|
||||
assertTrue(result is UtunConnection); owned.add(id to (result as UtunConnection))
|
||||
assertSame(result, TelecomCallManager.connectionFor(id)); assertEquals(2, result.state)
|
||||
}
|
||||
@Test fun sdkRequestFailuresReturnFalseWithoutCrashingTheCaller() {
|
||||
TelecomCallManager.init(); TelecomManager.shared!!.failures = setOf("place", "incoming")
|
||||
assertFalse(TelecomCallManager.placeCall(17, 99, "peer")); assertFalse(TelecomCallManager.addIncomingCall(17, 99, "peer"))
|
||||
}
|
||||
@Test fun duplicateRegistrationRetainsTheOriginalAndDestroysTheRejectedOwner() {
|
||||
val first = registered(); val duplicate = connection()
|
||||
assertFalse(TelecomCallManager.registerConnection(17, duplicate)); assertSame(first, TelecomCallManager.connectionFor(17))
|
||||
assertTrue(duplicate.destroyed); assertFalse(first.destroyed); assertEquals(listOf("available:17"), events)
|
||||
}
|
||||
@Test fun lateRegistrationIsRejectedAndRemainsOwnedIfDestroyFails() {
|
||||
val late = connection(); late.failures = setOf("destroy")
|
||||
assertFalse(TelecomCallManager.registerConnection(17, late)); assertFalse(late.destroyed)
|
||||
late.failures = emptySet(); main.advanceBy(250); assertTrue(late.destroyed)
|
||||
}
|
||||
@Test fun stateFailureDuringRegistrationRetiresTheConnectionAndReportsFailure() {
|
||||
current(); val conn = connection(); conn.failures = setOf("active")
|
||||
assertFalse(TelecomCallManager.registerConnection(17, conn)); main.advanceBy(0)
|
||||
assertTrue(conn.destroyed); assertNull(TelecomCallManager.connectionFor(17)); assertTrue("failed:17" in events)
|
||||
}
|
||||
@Test fun availableListenerFailureDoesNotLoseTheRegisteredConnection() {
|
||||
current(); val conn = connection(); failAvailable = true
|
||||
assertFalse(TelecomCallManager.registerConnection(17, conn)); assertTrue(conn.destroyed)
|
||||
main.advanceBy(0); assertTrue("failed:17" in events)
|
||||
}
|
||||
@Test fun disconnectFailureCannotSkipIndependentDestroy() {
|
||||
val conn = registered(); conn.failures = setOf("disconnected")
|
||||
TelecomCallManager.setDisconnected(17, 2); TelecomCallManager.removeConnection(17)
|
||||
assertTrue(conn.destroyed); assertNull(TelecomCallManager.connectionFor(17))
|
||||
}
|
||||
@Test fun rejectedRetryTimerRetainsTheOwnerForExplicitRemovalRetry() {
|
||||
val conn = registered(); conn.failures = setOf("destroy"); Handler.rejectDelayed = true
|
||||
TelecomCallManager.setDisconnected(17, 2); TelecomCallManager.removeConnection(17); assertFalse(conn.destroyed)
|
||||
conn.failures = emptySet(); Handler.rejectDelayed = false; TelecomCallManager.removeConnection(17); assertTrue(conn.destroyed)
|
||||
}
|
||||
@Test fun destroyFailureAfterEffectStillNeedsAnAcknowledgedRetry() {
|
||||
val conn = registered(); conn.failures = setOf("destroy after")
|
||||
TelecomCallManager.removeConnection(17); assertTrue(conn.destroyed)
|
||||
conn.failures = emptySet(); main.advanceBy(250); TelecomCallManager.removeConnection(17)
|
||||
}
|
||||
@Test fun systemInputAndRouteRequestsAfterDisconnectAreIgnored() {
|
||||
val conn = routed(); TelecomCallManager.setDisconnected(17, 0)
|
||||
conn.onAnswer(); conn.onReject(); conn.onDisconnect(); conn.onAbort(); conn.onShowIncomingCallUi(); conn.routeTo(AudioRoute.SPEAKER)
|
||||
main.advanceBy(0); assertEquals(listOf("available:17"), events); assertTrue(conn.endpointRequests.isEmpty())
|
||||
}
|
||||
@Test fun endpointResultDoesNotOptimisticallyConfirmTheAudioRoute() {
|
||||
val conn = routed(); conn.routeTo(AudioRoute.SPEAKER); conn.endpointRequests.single().succeed(); main.advanceBy(0)
|
||||
assertEquals(AudioRoute.EARPIECE, conn.confirmedRoute)
|
||||
conn.onCallEndpointChanged(speaker); assertEquals(AudioRoute.SPEAKER, conn.confirmedRoute)
|
||||
}
|
||||
@Test fun latestDesiredRouteIsSentAfterTheOutstandingEndpointRequestCompletes() {
|
||||
val conn = routed(); conn.routeTo(AudioRoute.SPEAKER); conn.routeTo(AudioRoute.EARPIECE)
|
||||
assertEquals(1, conn.endpointRequests.size); conn.endpointRequests.first().succeed(); main.advanceBy(0)
|
||||
assertEquals(2, conn.endpointRequests.size); assertEquals(earpiece, conn.endpointRequests.last().endpoint)
|
||||
conn.onCallEndpointChanged(speaker); conn.endpointRequests.last().succeed(); main.advanceBy(0); conn.onCallEndpointChanged(earpiece)
|
||||
assertEquals(AudioRoute.EARPIECE, conn.confirmedRoute)
|
||||
}
|
||||
@Test fun outcomeAfterDisconnectCannotRequestAnotherEndpoint() {
|
||||
val conn = routed(); conn.routeTo(AudioRoute.SPEAKER); conn.routeTo(AudioRoute.EARPIECE)
|
||||
TelecomCallManager.setDisconnected(17, 0); conn.endpointRequests.single().succeed(); main.advanceBy(0)
|
||||
assertEquals(1, conn.endpointRequests.size)
|
||||
}
|
||||
@Test fun routeListenerFailuresDoNotEscapeIntoTheSdk() {
|
||||
val conn = registered(); conn.onRouteChanged = { error("route callback failed") }; conn.onEndpointsChanged = { error("endpoints callback failed") }
|
||||
conn.onAvailableCallEndpointsChanged(mutableListOf(earpiece)); conn.onCallEndpointChanged(earpiece)
|
||||
assertEquals(AudioRoute.EARPIECE, conn.confirmedRoute)
|
||||
}
|
||||
@Test fun legacyRouteWaitsForAudioStateAndThenUsesTheSupportedHeadset() {
|
||||
Build.VERSION.SDK_INT = 33; val conn = registered(); conn.routeTo(AudioRoute.HEADSET); assertTrue(conn.legacyRequests.isEmpty())
|
||||
conn.callAudioState = CallAudioState(CallAudioState.ROUTE_SPEAKER, CallAudioState.ROUTE_SPEAKER or CallAudioState.ROUTE_WIRED_HEADSET)
|
||||
conn.onCallAudioStateChanged(conn.callAudioState); assertEquals(listOf(CallAudioState.ROUTE_WIRED_HEADSET), conn.legacyRequests)
|
||||
conn.callAudioState = CallAudioState(CallAudioState.ROUTE_WIRED_HEADSET, 12); conn.onCallAudioStateChanged(conn.callAudioState)
|
||||
assertEquals(AudioRoute.HEADSET, conn.confirmedRoute)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user