diff options
3 files changed, 172 insertions, 98 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 18b70731700a..fd943d0a5414 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1869,6 +1869,8 @@ <!-- Text displayed indicating that the user is connected to a satellite signal. --> <string name="satellite_connected_carrier_text">Satellite SOS</string> + <!-- Text displayed indicating that the user might be able to use satellite SOS. --> + <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS</string> <!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] --> <string name="accessibility_managed_profile">Work profile</string> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt index 199b5b672140..37f2f195ebf6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt @@ -36,14 +36,12 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** @@ -76,37 +74,10 @@ constructor( @DeviceBasedSatelliteInputLog logBuffer: LogBuffer, @DeviceBasedSatelliteTableLog tableLog: TableLogBuffer, ) : DeviceBasedSatelliteViewModel { - private val shouldShowIcon: Flow<Boolean> = - interactor.areAllConnectionsOutOfService - .flatMapLatest { allOos -> - if (!allOos) { - flowOf(false) - } else { - combine( - interactor.isSatelliteAllowed, - interactor.isSatelliteProvisioned, - interactor.isWifiActive, - airplaneModeRepository.isAirplaneMode - ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode -> - isSatelliteAllowed && - isSatelliteProvisioned && - !isWifiActive && - !isAirplaneMode - } - } - } - .distinctUntilChanged() - .logDiffsForTable( - tableLog, - columnPrefix = "vm", - columnName = COL_VISIBLE_CONDITION, - initialValue = false, - ) // This adds a 10 seconds delay before showing the icon - private val shouldActuallyShowIcon: StateFlow<Boolean> = - shouldShowIcon - .distinctUntilChanged() + private val shouldShowIconForOosAfterHysteresis: StateFlow<Boolean> = + interactor.areAllConnectionsOutOfService .flatMapLatest { shouldShow -> if (shouldShow) { logBuffer.log( @@ -125,6 +96,45 @@ constructor( .logDiffsForTable( tableLog, columnPrefix = "vm", + columnName = COL_VISIBLE_FOR_OOS, + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + private val canShowIcon = + combine( + interactor.isSatelliteAllowed, + interactor.isSatelliteProvisioned, + ) { allowed, provisioned -> + allowed && provisioned + } + + private val showIcon = + canShowIcon + .flatMapLatest { canShow -> + if (!canShow) { + flowOf(false) + } else { + combine( + shouldShowIconForOosAfterHysteresis, + interactor.connectionState, + interactor.isWifiActive, + airplaneModeRepository.isAirplaneMode, + ) { showForOos, connectionState, isWifiActive, isAirplaneMode -> + if (isWifiActive || isAirplaneMode) { + false + } else { + showForOos || + connectionState == SatelliteConnectionState.On || + connectionState == SatelliteConnectionState.Connected + } + } + } + } + .distinctUntilChanged() + .logDiffsForTable( + tableLog, + columnPrefix = "vm", columnName = COL_VISIBLE, initialValue = false, ) @@ -132,7 +142,7 @@ constructor( override val icon: StateFlow<Icon?> = combine( - shouldActuallyShowIcon, + showIcon, interactor.connectionState, interactor.signalStrength, ) { shouldShow, state, signalStrength -> @@ -146,7 +156,7 @@ constructor( override val carrierText: StateFlow<String?> = combine( - shouldActuallyShowIcon, + showIcon, interactor.connectionState, ) { shouldShow, connectionState -> logBuffer.log( @@ -156,7 +166,7 @@ constructor( bool1 = shouldShow str1 = connectionState.name }, - { "Updating carrier text. shouldActuallyShow=$bool1 connectionState=$str1" } + { "Updating carrier text. shouldShow=$bool1 connectionState=$str1" } ) if (shouldShow) { when (connectionState) { @@ -165,28 +175,30 @@ constructor( context.getString(R.string.satellite_connected_carrier_text) SatelliteConnectionState.Off, SatelliteConnectionState.Unknown -> { - null + // If we're showing the satellite icon opportunistically, use the + // emergency-only version of the carrier string + context.getString(R.string.satellite_emergency_only_carrier_text) } } } else { null } } - .onEach { - logBuffer.log( - TAG, - LogLevel.INFO, - { str1 = it }, - { "Resulting carrier text = $str1" } - ) - } + .distinctUntilChanged() + .logDiffsForTable( + tableLog, + columnPrefix = "vm", + columnName = COL_CARRIER_TEXT, + initialValue = null, + ) .stateIn(scope, SharingStarted.WhileSubscribed(), null) companion object { private const val TAG = "DeviceBasedSatelliteViewModel" private val DELAY_DURATION = 10.seconds - const val COL_VISIBLE_CONDITION = "visCondition" + const val COL_VISIBLE_FOR_OOS = "visibleForOos" const val COL_VISIBLE = "visible" + const val COL_CARRIER_TEXT = "carrierText" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt index c3cc33ffb81a..bf31f1e6d569 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -45,6 +45,7 @@ import org.junit.runner.RunWith import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { @@ -88,7 +89,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun icon_nullWhenShouldNotShow_satelliteNotAllowed() = + fun icon_null_satelliteNotAllowed() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -108,7 +109,30 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun icon_nullWhenShouldNotShow_notAllOos() = + fun icon_null_connectedAndNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is not allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN satellite state is Connected. (this should not ever occur, but still) + repo.connectionState.value = SatelliteConnectionState.Connected + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // THEN icon is null despite the connected state + assertThat(latest).isNull() + } + + @Test + fun icon_null_notAllOos() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -127,9 +151,28 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun icon_nullWhenShouldNotShow_isEmergencyOnly() = + fun icon_null_allOosAndNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // THEN icon is null because it is not allowed + assertThat(latest).isNull() + } + + @Test + fun icon_null_isEmergencyOnly() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -158,7 +201,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun icon_nullWhenShouldNotShow_apmIsEnabled() = + fun icon_null_apmIsEnabled() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -177,9 +220,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun icon_satelliteIsOn() = + fun icon_notNull_satelliteAllowedAndAllOos() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -201,7 +243,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(Icon::class.java) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun icon_hysteresisWhenEnablingIcon() = testScope.runTest { @@ -234,9 +275,56 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun icon_deviceIsProvisioned() = + fun icon_ignoresHysteresis_whenConnected() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // GIVEN satellite reports that it is Connected + repo.connectionState.value = SatelliteConnectionState.Connected + + // THEN icon is non null because we are connected, despite the normal OOS icon waiting + // 10 seconds for hysteresis + assertThat(latest).isInstanceOf(Icon::class.java) + } + + @Test + fun icon_ignoresHysteresis_whenOn() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // GIVEN satellite reports that it is Connected + repo.connectionState.value = SatelliteConnectionState.On + + // THEN icon is non null because the connection state is On, despite the normal OOS icon + // waiting 10 seconds for hysteresis + assertThat(latest).isInstanceOf(Icon::class.java) + } + + @Test + fun icon_satelliteIsProvisioned() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -267,7 +355,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(Icon::class.java) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun icon_wifiIsActive() = testScope.runTest { @@ -324,13 +411,13 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun carrierText_nullWhenShouldNotShow_notAllOos() = + fun carrierText_null_notAllOos() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) - // GIVEN satellite is allowed + connected + // GIVEN satellite is allowed + off repo.isSatelliteAllowedForCurrentLocation.value = true - repo.connectionState.value = SatelliteConnectionState.Connected + repo.connectionState.value = SatelliteConnectionState.Off // GIVEN all icons are not OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) @@ -344,9 +431,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_nullWhenShouldNotShow_isEmergencyOnly() = + fun carrierText_notNull_notAllOos_butConnected() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -354,25 +440,17 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { repo.isSatelliteAllowedForCurrentLocation.value = true repo.connectionState.value = SatelliteConnectionState.Connected - // GIVEN all icons are OOS + // GIVEN all icons are not OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = false + i1.isInService.value = true i1.isEmergencyOnly.value = false // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) - // Wait for delay to be completed - advanceTimeBy(10.seconds) - - // THEN carrier text is set because we don't have service + // THEN carrier text is not null, because it is connected + // This case should never happen, but let's test it anyway assertThat(latest).isNotNull() - - // GIVEN the connection is emergency only - i1.isEmergencyOnly.value = true - - // THEN carrier text is null because we have emergency connection - assertThat(latest).isNull() } @Test @@ -396,7 +474,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_satelliteIsOn() = testScope.runTest { @@ -421,9 +498,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNotNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_hysteresisWhenEnablingText() = + fun carrierText_noHysteresisWhenEnablingText_connected() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -439,23 +515,10 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) - // THEN carrier text is null because of the hysteresis - assertThat(latest).isNull() - - // Wait for delay to be completed - advanceTimeBy(10.seconds) - - // THEN carrier text is set after the delay + // THEN carrier text is not null because we skip hysteresis when connected assertThat(latest).isNotNull() - - // GIVEN apm is enabled - airplaneModeRepository.setIsAirplaneMode(true) - - // THEN carrier text is null immediately - assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_deviceIsProvisioned() = testScope.runTest { @@ -489,7 +552,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNotNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_wifiIsActive() = testScope.runTest { @@ -526,9 +588,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNotNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_connectionStateUnknown_null() = + fun carrierText_connectionStateUnknown_usesEmergencyOnlyText() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -544,12 +605,12 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // Wait for delay to be completed advanceTimeBy(10.seconds) - assertThat(latest).isNull() + assertThat(latest) + .isEqualTo(context.getString(R.string.satellite_emergency_only_carrier_text)) } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_connectionStateOff_null() = + fun carrierText_connectionStateOff_usesEmergencyOnlyText() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -565,10 +626,10 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // Wait for delay to be completed advanceTimeBy(10.seconds) - assertThat(latest).isNull() + assertThat(latest) + .isEqualTo(context.getString(R.string.satellite_emergency_only_carrier_text)) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_connectionStateOn_notConnectedString() = testScope.runTest { @@ -590,7 +651,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { .isEqualTo(context.getString(R.string.satellite_connected_carrier_text)) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_connectionStateConnected_connectedString() = testScope.runTest { |