diff options
9 files changed, 100 insertions, 109 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ServiceStateModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ServiceStateModel.kt deleted file mode 100644 index cce3eb02023b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ServiceStateModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.mobile.data.model - -import android.telephony.ServiceState - -/** - * Simplified representation of a [ServiceState] for use in SystemUI. Add any fields that we need to - * extract from service state here for consumption downstream - */ -data class ServiceStateModel(val isEmergencyOnly: Boolean) { - companion object { - fun fromServiceState(serviceState: ServiceState): ServiceStateModel { - return ServiceStateModel(isEmergencyOnly = serviceState.isEmergencyOnly) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 5ad8bf1652b6..32e9c85bea81 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -21,7 +21,6 @@ import android.telephony.SubscriptionManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -93,17 +92,15 @@ interface MobileConnectionsRepository { val defaultMobileIconGroup: Flow<MobileIconGroup> /** - * [deviceServiceState] is equivalent to the last [Intent.ACTION_SERVICE_STATE] broadcast with a - * subscriptionId of -1 (aka [SubscriptionManager.INVALID_SUBSCRIPTION_ID]). + * Can the device make emergency calls using the device-based service state? This field is only + * useful when all known active subscriptions are OOS and not emergency call capable. * - * While each [MobileConnectionsRepository] listens for the service state of each subscription, - * there is potentially a service state associated with the device itself. This value can be - * used to calculate e.g., the emergency calling capability of the device (as opposed to the - * emergency calling capability of an individual mobile connection) + * Specifically, this checks every [ServiceState] of the device, and looks for any that report + * [ServiceState.isEmergencyOnly]. * - * Note: this is a [StateFlow] using an eager sharing strategy. + * This is an eager flow, and re-evaluates whenever ACTION_SERVICE_STATE is sent for subId = -1. */ - val deviceServiceState: StateFlow<ServiceStateModel?> + val isDeviceEmergencyCallCapable: StateFlow<Boolean> /** * If any active SIM on the device is in diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index b0681525a137..b247da4f4219 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -25,7 +25,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl @@ -152,16 +151,17 @@ constructor( override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> = activeRepo.flatMapLatest { it.defaultMobileIconGroup } - override val deviceServiceState: StateFlow<ServiceStateModel?> = + override val isDeviceEmergencyCallCapable: StateFlow<Boolean> = activeRepo - .flatMapLatest { it.deviceServiceState } + .flatMapLatest { it.isDeviceEmergencyCallCapable } .stateIn( scope, SharingStarted.WhileSubscribed(), - realRepository.deviceServiceState.value + realRepository.isDeviceEmergencyCallCapable.value ) override val isAnySimSecure: Flow<Boolean> = activeRepo.flatMapLatest { it.isAnySimSecure } + override fun getIsAnySimSecure(): Boolean = activeRepo.value.getIsAnySimSecure() override val defaultDataSubId: StateFlow<Int> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index a944e9133a31..3a79f3fb3573 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -27,7 +27,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository @@ -137,10 +136,11 @@ constructor( override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G) - // TODO(b/339023069): demo command for device-based connectivity state - override val deviceServiceState: StateFlow<ServiceStateModel?> = MutableStateFlow(null) + // TODO(b/339023069): demo command for device-based emergency calls state + override val isDeviceEmergencyCallCapable: StateFlow<Boolean> = MutableStateFlow(false) override val isAnySimSecure: Flow<Boolean> = flowOf(getIsAnySimSecure()) + override fun getIsAnySimSecure(): Boolean = false override val defaultMobileIconMapping = MutableStateFlow(TelephonyIcons.ICON_NAME_TO_ICON) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 261258a58914..b756a05da182 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.telephony.CarrierConfigManager -import android.telephony.ServiceState import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID @@ -49,7 +48,6 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.Airplane import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy @@ -72,7 +70,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -175,8 +172,8 @@ constructor( } .flowOn(bgDispatcher) - /** Note that this flow is eager, so we don't miss any state */ - override val deviceServiceState: StateFlow<ServiceStateModel?> = + /** Turn ACTION_SERVICE_STATE (for subId = -1) into an event */ + private val serviceStateChangedEvent: Flow<Unit> = broadcastDispatcher .broadcastFlow(IntentFilter(Intent.ACTION_SERVICE_STATE)) { intent, _ -> val subId = @@ -185,24 +182,34 @@ constructor( INVALID_SUBSCRIPTION_ID ) - val extras = intent.extras - if (extras == null) { - logger.logTopLevelServiceStateBroadcastMissingExtras(subId) - return@broadcastFlow null - } - - val serviceState = ServiceState.newFromBundle(extras) - logger.logTopLevelServiceStateBroadcastEmergencyOnly(subId, serviceState) + // Only emit if the subId is not associated with an active subscription if (subId == INVALID_SUBSCRIPTION_ID) { - // Assume that -1 here is the device's service state. We don't care about - // other ones. - ServiceStateModel.fromServiceState(serviceState) - } else { - null + Unit } } - .filterNotNull() - .stateIn(scope, SharingStarted.Eagerly, null) + // Emit on start so that we always check the state at least once + .onStart { emit(Unit) } + + /** Eager flow to determine the device-based emergency calls only state */ + override val isDeviceEmergencyCallCapable: StateFlow<Boolean> = + serviceStateChangedEvent + .mapLatest { + val modems = telephonyManager.activeModemCount + // Check the service state for every modem. If any state reports emergency calling + // capable, then consider the device to have emergency call capabilities + (0..<modems) + .map { telephonyManager.getServiceStateForSlot(it) } + .any { it?.isEmergencyOnly == true } + } + .flowOn(bgDispatcher) + .distinctUntilChanged() + .logDiffsForTable( + tableLogger, + columnPrefix = LOGGING_PREFIX, + columnName = "deviceEmergencyOnly", + initialValue = false, + ) + .stateIn(scope, SharingStarted.Eagerly, false) /** * State flow that emits the set of mobile data subscriptions, each represented by its own diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 26553e66ac5c..28fff4e68935 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -385,15 +385,7 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> = - mobileConnectionsRepo.deviceServiceState - .map { it?.isEmergencyOnly ?: false } - .distinctUntilChanged() - .logDiffsForTable( - tableLogger, - columnPrefix = LOGGING_PREFIX, - columnName = "deviceEmergencyOnly", - initialValue = false, - ) + mobileConnectionsRepo.isDeviceEmergencyCallCapable /** Vends out new [MobileIconInteractor] for a particular subId */ override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 76982ae12516..6de2caa59dd3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -28,7 +28,6 @@ import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo import android.net.wifi.WifiManager -import android.os.Bundle import android.os.ParcelUuid import android.telephony.CarrierConfigManager import android.telephony.ServiceState @@ -56,7 +55,6 @@ import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository @@ -74,7 +72,6 @@ import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.wifitrackerlib.MergedCarrierEntry import com.android.wifitrackerlib.WifiEntry @@ -98,6 +95,7 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @@ -602,47 +600,85 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @SuppressLint("UnspecifiedRegisterReceiverFlag") @Test - fun testDeviceServiceStateFromBroadcast_eagerlyWatchesBroadcast() = + fun testDeviceEmergencyCallState_eagerlyChecksState() = testScope.runTest { - // Value starts out empty (null) - assertThat(underTest.deviceServiceState.value).isNull() + // Value starts out false + assertThat(underTest.isDeviceEmergencyCallCapable.value).isFalse() + whenever(telephonyManager.activeModemCount).thenReturn(1) + whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { _ -> + ServiceState().apply { isEmergencyOnly = true } + } // WHEN an appropriate intent gets sent out - val intent = serviceStateIntent(subId = -1, emergencyOnly = false) + val intent = serviceStateIntent(subId = -1) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, intent, ) runCurrent() - // THEN the repo's state is updated - val expected = ServiceStateModel(isEmergencyOnly = false) - assertThat(underTest.deviceServiceState.value).isEqualTo(expected) + // THEN the repo's state is updated despite no listeners + assertThat(underTest.isDeviceEmergencyCallCapable.value).isEqualTo(true) } @Test - fun testDeviceServiceStateFromBroadcast_followsSubIdNegativeOne() = + fun testDeviceEmergencyCallState_aggregatesAcrossSlots_oneTrue() = testScope.runTest { - // device based state tracks -1 - val intent = serviceStateIntent(subId = -1, emergencyOnly = false) + val latest by collectLastValue(underTest.isDeviceEmergencyCallCapable) + + // GIVEN there are multiple slots + whenever(telephonyManager.activeModemCount).thenReturn(4) + // GIVEN only one of them reports ECM + whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { invocation -> + when (invocation.getArgument(0) as Int) { + 0 -> ServiceState().apply { isEmergencyOnly = false } + 1 -> ServiceState().apply { isEmergencyOnly = false } + 2 -> ServiceState().apply { isEmergencyOnly = true } + 3 -> ServiceState().apply { isEmergencyOnly = false } + else -> null + } + } + + // GIVEN a broadcast goes out for the appropriate subID + val intent = serviceStateIntent(subId = -1) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, intent, ) runCurrent() - val deviceBasedState = ServiceStateModel(isEmergencyOnly = false) - assertThat(underTest.deviceServiceState.value).isEqualTo(deviceBasedState) + // THEN the device is in ECM, because one of the service states is + assertThat(latest).isTrue() + } + + @Test + fun testDeviceEmergencyCallState_aggregatesAcrossSlots_allFalse() = + testScope.runTest { + val latest by collectLastValue(underTest.isDeviceEmergencyCallCapable) + + // GIVEN there are multiple slots + whenever(telephonyManager.activeModemCount).thenReturn(4) + // GIVEN only one of them reports ECM + whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { invocation -> + when (invocation.getArgument(0) as Int) { + 0 -> ServiceState().apply { isEmergencyOnly = false } + 1 -> ServiceState().apply { isEmergencyOnly = false } + 2 -> ServiceState().apply { isEmergencyOnly = false } + 3 -> ServiceState().apply { isEmergencyOnly = false } + else -> null + } + } - // ... and ignores any other subId - val intent2 = serviceStateIntent(subId = 1, emergencyOnly = true) + // GIVEN a broadcast goes out for the appropriate subID + val intent = serviceStateIntent(subId = -1) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, - intent2, + intent, ) runCurrent() - assertThat(underTest.deviceServiceState.value).isEqualTo(deviceBasedState) + // THEN the device is in ECM, because one of the service states is + assertThat(latest).isFalse() } @Test @@ -1549,15 +1585,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { */ private fun serviceStateIntent( subId: Int, - emergencyOnly: Boolean = false, ): Intent { - val serviceState = ServiceState().apply { isEmergencyOnly = emergencyOnly } - - val bundle = Bundle() - serviceState.fillInNotifierBundle(bundle) - return Intent(Intent.ACTION_SERVICE_STATE).apply { - putExtras(bundle) putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index cc0eae7e6fea..e218fba1d07a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -29,7 +29,6 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -897,13 +896,11 @@ class MobileIconsInteractorTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode) - connectionsRepository.deviceServiceState.value = - ServiceStateModel(isEmergencyOnly = true) + connectionsRepository.isDeviceEmergencyCallCapable.value = true assertThat(latest).isTrue() - connectionsRepository.deviceServiceState.value = - ServiceStateModel(isEmergencyOnly = false) + connectionsRepository.isDeviceEmergencyCallCapable.value = false assertThat(latest).isFalse() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 8229575a128f..e7be639cf92a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -23,7 +23,6 @@ import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy @@ -94,9 +93,10 @@ class FakeMobileConnectionsRepository( private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON) override val defaultMobileIconGroup = _defaultMobileIconGroup - override val deviceServiceState = MutableStateFlow<ServiceStateModel?>(null) + override val isDeviceEmergencyCallCapable = MutableStateFlow(false) override val isAnySimSecure = MutableStateFlow(false) + override fun getIsAnySimSecure(): Boolean = isAnySimSecure.value private var isInEcmMode: Boolean = false |