diff options
3 files changed, 149 insertions, 6 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index 12f252d215a9..9d9cc2aa11de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -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 @@ -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 @@ -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 @@ -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 } } @@ -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, @@ -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) @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt index 7ca3b1c425d3..6300953c86b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt @@ -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 @@ -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(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 6b0ad4bdb770..66516769c804 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -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 @@ -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 @@ -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 @@ -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() @@ -86,6 +91,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { underTest = DeviceBasedSatelliteRepositoryImpl( Optional.empty(), + telephonyManager, dispatcher, testScope.backgroundScope, FakeLogBuffer.Factory.create(), @@ -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, @@ -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(), |