Loading packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +77 −6 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS Loading @@ -38,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupp import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import java.util.Optional import javax.inject.Inject Loading @@ -51,12 +54,15 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine Loading Loading @@ -92,13 +98,19 @@ sealed interface SatelliteSupport { @OptIn(ExperimentalCoroutinesApi::class) companion object { /** Convenience function to switch to the supported flow */ /** * Convenience function to switch to the supported flow. [retrySignal] is a flow that emits * [Unit] whenever the [supported] flow needs to be restarted */ fun <T> Flow<SatelliteSupport>.whenSupported( supported: (SatelliteManager) -> Flow<T>, orElse: Flow<T>, ): Flow<T> = flatMapLatest { when (it) { is Supported -> supported(it.satelliteManager) retrySignal: Flow<Unit>, ): Flow<T> = flatMapLatest { satelliteSupport -> when (satelliteSupport) { is Supported -> { retrySignal.flatMapLatest { supported(satelliteSupport.satelliteManager) } } else -> orElse } } Loading Loading @@ -132,6 +144,7 @@ class DeviceBasedSatelliteRepositoryImpl @Inject constructor( satelliteManagerOpt: Optional<SatelliteManager>, telephonyManager: TelephonyManager, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, @OemSatelliteInputLog private val logBuffer: LogBuffer, Loading Loading @@ -201,11 +214,65 @@ constructor( } } /** * Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a * specific `subscriptionId`). Therefore this is the radio power state of the * DEFAULT_SUBSCRIPTION_ID subscription. This subscription, I am led to believe, is the one that * would be used for the SatelliteManager subscription. * * By watching power state changes, we can detect if the telephony process crashes. * * See b/337258696 for details */ private val radioPowerState: StateFlow<Int> = conflatedCallbackFlow { val cb = object : TelephonyCallback(), TelephonyCallback.RadioPowerStateListener { override fun onRadioPowerStateChanged(powerState: Int) { trySend(powerState) } } telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), cb) awaitClose { telephonyManager.unregisterTelephonyCallback(cb) } } .flowOn(bgDispatcher) .stateIn( scope, SharingStarted.WhileSubscribed(), TelephonyManager.RADIO_POWER_UNAVAILABLE ) /** * In the event that a telephony phone process has crashed, we expect to see a radio power state * change from ON to something else. This trigger can be used to re-start a flow via * [whenSupported] * * This flow emits [Unit] when started so that newly-started collectors always run, and only * restart when the state goes from ON -> !ON */ private val telephonyProcessCrashedEvent: Flow<Unit> = radioPowerState .pairwise() .mapNotNull { (prev: Int, new: Int) -> if ( prev == TelephonyManager.RADIO_POWER_ON && new != TelephonyManager.RADIO_POWER_ON ) { Unit } else { null } } .onStart { emit(Unit) } override val connectionState = satelliteSupport .whenSupported( supported = ::connectionStateFlow, orElse = flowOf(SatelliteConnectionState.Off) orElse = flowOf(SatelliteConnectionState.Off), retrySignal = telephonyProcessCrashedEvent, ) .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off) Loading @@ -232,7 +299,11 @@ constructor( override val signalStrength = satelliteSupport .whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0)) .whenSupported( supported = ::signalStrengthFlow, orElse = flowOf(0), retrySignal = telephonyProcessCrashedEvent, ) .stateIn(scope, SharingStarted.Eagerly, 0) // By using the SupportedSatelliteManager here, we expect registration never to fail Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt +3 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.satellite.data import android.telephony.TelephonyManager import android.telephony.satellite.SatelliteManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase Loading Loading @@ -50,11 +51,13 @@ class DeviceBasedSatelliteRepositorySwitcherTest : SysuiTestCase() { private val demoModeController = mock<DemoModeController>().apply { whenever(this.isInDemoMode).thenReturn(false) } private val satelliteManager = mock<SatelliteManager>() private val telephonyManager = mock<TelephonyManager>() private val systemClock = FakeSystemClock() private val realImpl = DeviceBasedSatelliteRepositoryImpl( Optional.of(satelliteManager), telephonyManager, testDispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +69 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.os.Process import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrength import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager Loading @@ -36,6 +38,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState Loading @@ -59,6 +62,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.doAnswer import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations Loading @@ -69,6 +73,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl @Mock private lateinit var satelliteManager: SatelliteManager @Mock private lateinit var telephonyManager: TelephonyManager private val systemClock = FakeSystemClock() private val dispatcher = StandardTestDispatcher() Loading @@ -86,6 +91,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { underTest = DeviceBasedSatelliteRepositoryImpl( Optional.empty(), telephonyManager, dispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), Loading Loading @@ -362,6 +368,68 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { verify(satelliteManager).registerForModemStateChanged(any(), any()) } @Test fun telephonyCrash_repoReregistersConnectionStateListener() = testScope.runTest { setupDefaultRepo() // GIVEN connection state is requested val connectionState by collectLastValue(underTest.connectionState) runCurrent() val telephonyCallback = MobileTelephonyHelpers.getTelephonyCallbackForType< TelephonyCallback.RadioPowerStateListener >( telephonyManager ) // THEN listener is registered once verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any()) // WHEN a crash event happens (detected by radio state change) telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON) runCurrent() telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF) runCurrent() // THEN listeners are unregistered and re-registered verify(satelliteManager, times(1)).unregisterForModemStateChanged(any()) verify(satelliteManager, times(2)).registerForModemStateChanged(any(), any()) } @Test fun telephonyCrash_repoReregistersSignalStrengthListener() = testScope.runTest { setupDefaultRepo() // GIVEN signal strength is requested val signalStrength by collectLastValue(underTest.signalStrength) runCurrent() val telephonyCallback = MobileTelephonyHelpers.getTelephonyCallbackForType< TelephonyCallback.RadioPowerStateListener >( telephonyManager ) // THEN listeners are registered the first time verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any()) // WHEN a crash event happens (detected by radio state change) telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON) runCurrent() telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF) runCurrent() // THEN listeners are unregistered and re-registered verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any()) verify(satelliteManager, times(2)).registerForNtnSignalStrengthChanged(any(), any()) } private fun setUpRepo( uptime: Long = MIN_UPTIME, satMan: SatelliteManager? = satelliteManager, Loading @@ -380,6 +448,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { underTest = DeviceBasedSatelliteRepositoryImpl( if (satMan != null) Optional.of(satMan) else Optional.empty(), telephonyManager, dispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +77 −6 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS Loading @@ -38,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupp import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import java.util.Optional import javax.inject.Inject Loading @@ -51,12 +54,15 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine Loading Loading @@ -92,13 +98,19 @@ sealed interface SatelliteSupport { @OptIn(ExperimentalCoroutinesApi::class) companion object { /** Convenience function to switch to the supported flow */ /** * Convenience function to switch to the supported flow. [retrySignal] is a flow that emits * [Unit] whenever the [supported] flow needs to be restarted */ fun <T> Flow<SatelliteSupport>.whenSupported( supported: (SatelliteManager) -> Flow<T>, orElse: Flow<T>, ): Flow<T> = flatMapLatest { when (it) { is Supported -> supported(it.satelliteManager) retrySignal: Flow<Unit>, ): Flow<T> = flatMapLatest { satelliteSupport -> when (satelliteSupport) { is Supported -> { retrySignal.flatMapLatest { supported(satelliteSupport.satelliteManager) } } else -> orElse } } Loading Loading @@ -132,6 +144,7 @@ class DeviceBasedSatelliteRepositoryImpl @Inject constructor( satelliteManagerOpt: Optional<SatelliteManager>, telephonyManager: TelephonyManager, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, @OemSatelliteInputLog private val logBuffer: LogBuffer, Loading Loading @@ -201,11 +214,65 @@ constructor( } } /** * Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a * specific `subscriptionId`). Therefore this is the radio power state of the * DEFAULT_SUBSCRIPTION_ID subscription. This subscription, I am led to believe, is the one that * would be used for the SatelliteManager subscription. * * By watching power state changes, we can detect if the telephony process crashes. * * See b/337258696 for details */ private val radioPowerState: StateFlow<Int> = conflatedCallbackFlow { val cb = object : TelephonyCallback(), TelephonyCallback.RadioPowerStateListener { override fun onRadioPowerStateChanged(powerState: Int) { trySend(powerState) } } telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), cb) awaitClose { telephonyManager.unregisterTelephonyCallback(cb) } } .flowOn(bgDispatcher) .stateIn( scope, SharingStarted.WhileSubscribed(), TelephonyManager.RADIO_POWER_UNAVAILABLE ) /** * In the event that a telephony phone process has crashed, we expect to see a radio power state * change from ON to something else. This trigger can be used to re-start a flow via * [whenSupported] * * This flow emits [Unit] when started so that newly-started collectors always run, and only * restart when the state goes from ON -> !ON */ private val telephonyProcessCrashedEvent: Flow<Unit> = radioPowerState .pairwise() .mapNotNull { (prev: Int, new: Int) -> if ( prev == TelephonyManager.RADIO_POWER_ON && new != TelephonyManager.RADIO_POWER_ON ) { Unit } else { null } } .onStart { emit(Unit) } override val connectionState = satelliteSupport .whenSupported( supported = ::connectionStateFlow, orElse = flowOf(SatelliteConnectionState.Off) orElse = flowOf(SatelliteConnectionState.Off), retrySignal = telephonyProcessCrashedEvent, ) .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off) Loading @@ -232,7 +299,11 @@ constructor( override val signalStrength = satelliteSupport .whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0)) .whenSupported( supported = ::signalStrengthFlow, orElse = flowOf(0), retrySignal = telephonyProcessCrashedEvent, ) .stateIn(scope, SharingStarted.Eagerly, 0) // By using the SupportedSatelliteManager here, we expect registration never to fail Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt +3 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.satellite.data import android.telephony.TelephonyManager import android.telephony.satellite.SatelliteManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase Loading Loading @@ -50,11 +51,13 @@ class DeviceBasedSatelliteRepositorySwitcherTest : SysuiTestCase() { private val demoModeController = mock<DemoModeController>().apply { whenever(this.isInDemoMode).thenReturn(false) } private val satelliteManager = mock<SatelliteManager>() private val telephonyManager = mock<TelephonyManager>() private val systemClock = FakeSystemClock() private val realImpl = DeviceBasedSatelliteRepositoryImpl( Optional.of(satelliteManager), telephonyManager, testDispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +69 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.os.Process import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrength import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager Loading @@ -36,6 +38,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState Loading @@ -59,6 +62,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.doAnswer import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations Loading @@ -69,6 +73,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl @Mock private lateinit var satelliteManager: SatelliteManager @Mock private lateinit var telephonyManager: TelephonyManager private val systemClock = FakeSystemClock() private val dispatcher = StandardTestDispatcher() Loading @@ -86,6 +91,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { underTest = DeviceBasedSatelliteRepositoryImpl( Optional.empty(), telephonyManager, dispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), Loading Loading @@ -362,6 +368,68 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { verify(satelliteManager).registerForModemStateChanged(any(), any()) } @Test fun telephonyCrash_repoReregistersConnectionStateListener() = testScope.runTest { setupDefaultRepo() // GIVEN connection state is requested val connectionState by collectLastValue(underTest.connectionState) runCurrent() val telephonyCallback = MobileTelephonyHelpers.getTelephonyCallbackForType< TelephonyCallback.RadioPowerStateListener >( telephonyManager ) // THEN listener is registered once verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any()) // WHEN a crash event happens (detected by radio state change) telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON) runCurrent() telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF) runCurrent() // THEN listeners are unregistered and re-registered verify(satelliteManager, times(1)).unregisterForModemStateChanged(any()) verify(satelliteManager, times(2)).registerForModemStateChanged(any(), any()) } @Test fun telephonyCrash_repoReregistersSignalStrengthListener() = testScope.runTest { setupDefaultRepo() // GIVEN signal strength is requested val signalStrength by collectLastValue(underTest.signalStrength) runCurrent() val telephonyCallback = MobileTelephonyHelpers.getTelephonyCallbackForType< TelephonyCallback.RadioPowerStateListener >( telephonyManager ) // THEN listeners are registered the first time verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any()) // WHEN a crash event happens (detected by radio state change) telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON) runCurrent() telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF) runCurrent() // THEN listeners are unregistered and re-registered verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any()) verify(satelliteManager, times(2)).registerForNtnSignalStrengthChanged(any(), any()) } private fun setUpRepo( uptime: Long = MIN_UPTIME, satMan: SatelliteManager? = satelliteManager, Loading @@ -380,6 +448,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { underTest = DeviceBasedSatelliteRepositoryImpl( if (satMan != null) Optional.of(satMan) else Optional.empty(), telephonyManager, dispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), Loading