diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 6c9ad6a2df7f0..fcd1b8abefe47 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -18,8 +18,8 @@ package com.android.systemui.statusbar.pipeline.dagger import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor @@ -45,9 +45,9 @@ abstract class StatusBarPipelineModule { abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository @Binds - abstract fun mobileSubscriptionRepository( - impl: MobileSubscriptionRepositoryImpl - ): MobileSubscriptionRepository + abstract fun mobileConnectionsRepository( + impl: MobileConnectionsRepositoryImpl + ): MobileConnectionsRepository @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt new file mode 100644 index 0000000000000..45284cf0332b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 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.repository + +import android.telephony.CellSignalStrength +import android.telephony.CellSignalStrengthCdma +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyCallback +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE +import android.telephony.TelephonyManager +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import java.lang.IllegalStateException +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +/** + * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a + * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this + * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId + * + * There should only ever be one [MobileConnectionRepository] per subscription, since + * [TelephonyManager] limits the number of callbacks that can be registered per process. + * + * This repository should have all of the relevant information for a single line of service, which + * eventually becomes a single icon in the status bar. + */ +interface MobileConnectionRepository { + /** + * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single + * listener + model. + */ + val subscriptionModelFlow: Flow +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class MobileConnectionRepositoryImpl( + private val subId: Int, + telephonyManager: TelephonyManager, + bgDispatcher: CoroutineDispatcher, + logger: ConnectivityPipelineLogger, + scope: CoroutineScope, +) : MobileConnectionRepository { + init { + if (telephonyManager.subscriptionId != subId) { + throw IllegalStateException( + "TelephonyManager should be created with subId($subId). " + + "Found ${telephonyManager.subscriptionId} instead." + ) + } + } + + override val subscriptionModelFlow: StateFlow = run { + var state = MobileSubscriptionModel() + conflatedCallbackFlow { + // TODO (b/240569788): log all of these into the connectivity logger + val callback = + object : + TelephonyCallback(), + TelephonyCallback.ServiceStateListener, + TelephonyCallback.SignalStrengthsListener, + TelephonyCallback.DataConnectionStateListener, + TelephonyCallback.DataActivityListener, + TelephonyCallback.CarrierNetworkListener, + TelephonyCallback.DisplayInfoListener { + override fun onServiceStateChanged(serviceState: ServiceState) { + state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) + trySend(state) + } + + override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + val cdmaLevel = + signalStrength + .getCellSignalStrengths(CellSignalStrengthCdma::class.java) + .let { strengths -> + if (!strengths.isEmpty()) { + strengths[0].level + } else { + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN + } + } + + val primaryLevel = signalStrength.level + + state = + state.copy( + cdmaLevel = cdmaLevel, + primaryLevel = primaryLevel, + isGsm = signalStrength.isGsm, + ) + trySend(state) + } + + override fun onDataConnectionStateChanged( + dataState: Int, + networkType: Int + ) { + state = state.copy(dataConnectionState = dataState) + trySend(state) + } + + override fun onDataActivity(direction: Int) { + state = state.copy(dataActivityDirection = direction) + trySend(state) + } + + override fun onCarrierNetworkChange(active: Boolean) { + state = state.copy(carrierNetworkChangeActive = active) + trySend(state) + } + + override fun onDisplayInfoChanged( + telephonyDisplayInfo: TelephonyDisplayInfo + ) { + val networkType = + if ( + telephonyDisplayInfo.overrideNetworkType == + OVERRIDE_NETWORK_TYPE_NONE + ) { + DefaultNetworkType(telephonyDisplayInfo.networkType) + } else { + OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType) + } + state = state.copy(resolvedNetworkType = networkType) + trySend(state) + } + } + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) } + .stateIn(scope, SharingStarted.WhileSubscribed(), state) + } + + class Factory + @Inject + constructor( + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + ) { + fun build(subId: Int): MobileConnectionRepository { + return MobileConnectionRepositoryImpl( + subId, + telephonyManager.createForSubscriptionId(subId), + bgDispatcher, + logger, + scope, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt similarity index 51% rename from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index b6adc406cce99..0e2428ae393a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -19,22 +19,10 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.content.Context import android.content.IntentFilter import android.telephony.CarrierConfigManager -import android.telephony.CellSignalStrength -import android.telephony.CellSignalStrengthCdma -import android.telephony.ServiceState -import android.telephony.SignalStrength import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener -import android.telephony.TelephonyCallback.CarrierNetworkListener -import android.telephony.TelephonyCallback.DataActivityListener -import android.telephony.TelephonyCallback.DataConnectionStateListener -import android.telephony.TelephonyCallback.DisplayInfoListener -import android.telephony.TelephonyCallback.ServiceStateListener -import android.telephony.TelephonyCallback.SignalStrengthsListener -import android.telephony.TelephonyDisplayInfo -import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE import android.telephony.TelephonyManager import androidx.annotation.VisibleForTesting import com.android.settingslib.mobile.MobileMappings @@ -44,10 +32,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType -import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -68,7 +52,7 @@ import kotlinx.coroutines.withContext * Repo for monitoring the complete active subscription info list, to be consumed and filtered based * on various policy */ -interface MobileSubscriptionRepository { +interface MobileConnectionsRepository { /** Observable list of current mobile subscriptions */ val subscriptionsFlow: Flow> @@ -78,14 +62,14 @@ interface MobileSubscriptionRepository { /** Observable for [MobileMappings.Config] tracking the defaults */ val defaultDataSubRatConfig: StateFlow - /** Get or create an observable for the given subscription ID */ - fun getFlowForSubId(subId: Int): Flow + /** Get or create a repository for the line of service for the given subscription ID */ + fun getRepoForSubId(subId: Int): MobileConnectionRepository } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton -class MobileSubscriptionRepositoryImpl +class MobileConnectionsRepositoryImpl @Inject constructor( private val subscriptionManager: SubscriptionManager, @@ -93,11 +77,11 @@ constructor( private val logger: ConnectivityPipelineLogger, broadcastDispatcher: BroadcastDispatcher, private val context: Context, - private val mobileMappings: MobileMappingsProxy, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, -) : MobileSubscriptionRepository { - private val subIdFlowCache: MutableMap> = mutableMapOf() + private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory +) : MobileConnectionsRepository { + private val subIdRepositoryCache: MutableMap = mutableMapOf() /** * State flow that emits the set of mobile data subscriptions, each represented by its own @@ -121,6 +105,7 @@ constructor( awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } } .mapLatest { fetchSubscriptionsList() } + .onEach { infos -> dropUnusedReposFromCache(infos) } .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) /** StateFlow that keeps track of the current active mobile data subscription */ @@ -172,103 +157,43 @@ constructor( initialValue = Config.readConfig(context) ) - /** - * Each mobile subscription needs its own flow, which comes from registering listeners on the - * system. Use this method to create those flows and cache them for reuse - */ - override fun getFlowForSubId(subId: Int): StateFlow { - return subIdFlowCache[subId] - ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it } + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + if (!isValidSubId(subId)) { + throw IllegalArgumentException( + "subscriptionId $subId is not in the list of valid subscriptions" + ) + } + + return subIdRepositoryCache[subId] + ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } } - @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache - - private fun createFlowForSubId(subId: Int): StateFlow = run { - var state = MobileSubscriptionModel() - conflatedCallbackFlow { - val phony = telephonyManager.createForSubscriptionId(subId) - // TODO (b/240569788): log all of these into the connectivity logger - val callback = - object : - TelephonyCallback(), - ServiceStateListener, - SignalStrengthsListener, - DataConnectionStateListener, - DataActivityListener, - CarrierNetworkListener, - DisplayInfoListener { - override fun onServiceStateChanged(serviceState: ServiceState) { - state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) - trySend(state) - } - - override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { - val cdmaLevel = - signalStrength - .getCellSignalStrengths(CellSignalStrengthCdma::class.java) - .let { strengths -> - if (!strengths.isEmpty()) { - strengths[0].level - } else { - CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN - } - } - - val primaryLevel = signalStrength.level - - state = - state.copy( - cdmaLevel = cdmaLevel, - primaryLevel = primaryLevel, - isGsm = signalStrength.isGsm, - ) - trySend(state) - } - - override fun onDataConnectionStateChanged( - dataState: Int, - networkType: Int - ) { - state = state.copy(dataConnectionState = dataState) - trySend(state) - } - - override fun onDataActivity(direction: Int) { - state = state.copy(dataActivityDirection = direction) - trySend(state) - } - - override fun onCarrierNetworkChange(active: Boolean) { - state = state.copy(carrierNetworkChangeActive = active) - trySend(state) - } - - override fun onDisplayInfoChanged( - telephonyDisplayInfo: TelephonyDisplayInfo - ) { - val networkType = - if ( - telephonyDisplayInfo.overrideNetworkType == - OVERRIDE_NETWORK_TYPE_NONE - ) { - DefaultNetworkType(telephonyDisplayInfo.networkType) - } else { - OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType) - } - - state = state.copy(resolvedNetworkType = networkType) - trySend(state) - } - } - phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) - awaitClose { - phony.unregisterTelephonyCallback(callback) - // Release the cached flow - subIdFlowCache.remove(subId) - } + private fun isValidSubId(subId: Int): Boolean { + subscriptionsFlow.value.forEach { + if (it.subscriptionId == subId) { + return true } - .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) } - .stateIn(scope, SharingStarted.WhileSubscribed(), state) + } + + return false + } + + @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + + private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { + return mobileConnectionRepositoryFactory.build(subId) + } + + private fun dropUnusedReposFromCache(newInfos: List) { + // Remove any connection repository from the cache that isn't in the new set of IDs. They + // will get garbage collected once their subscribers go away + val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } + + subIdRepositoryCache.keys.forEach { + if (!currentValidSubscriptionIds.contains(it)) { + subIdRepositoryCache.remove(it) + } + } } private suspend fun fetchSubscriptionsList(): List = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index e6d9993734d9c..15f4acc1127ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -19,8 +19,8 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import kotlinx.coroutines.flow.Flow @@ -50,8 +50,10 @@ class MobileIconInteractorImpl( defaultMobileIconMapping: Flow>, defaultMobileIconGroup: Flow, mobileMappingsProxy: MobileMappingsProxy, - mobileStatusInfo: Flow, + connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { + private val mobileStatusInfo = connectionRepository.subscriptionModelFlow + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: Flow = combine( 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 e401a9c78cf27..cd411a4a2afe1 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 @@ -23,8 +23,7 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker @@ -59,7 +58,7 @@ interface MobileIconsInteractor { class MobileIconsInteractorImpl @Inject constructor( - private val mobileSubscriptionRepo: MobileSubscriptionRepository, + private val mobileSubscriptionRepo: MobileConnectionsRepository, private val carrierConfigTracker: CarrierConfigTracker, private val mobileMappingsProxy: MobileMappingsProxy, userSetupRepo: UserSetupRepository, @@ -138,12 +137,6 @@ constructor( defaultMobileIconMapping, defaultMobileIconGroup, mobileMappingsProxy, - mobileSubscriptionFlowForSubId(subId), + mobileSubscriptionRepo.getRepoForSubId(subId), ) - - /** - * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections - */ - private fun mobileSubscriptionFlowForSubId(subId: Int): Flow = - mobileSubscriptionRepo.getFlowForSubId(subId) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt new file mode 100644 index 0000000000000..6ff7b7ccd5e30 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 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.repository + +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileConnectionRepository : MobileConnectionRepository { + private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) + override val subscriptionModelFlow: Flow = _subscriptionsModelFlow + + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { + _subscriptionsModelFlow.value = model + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt similarity index 72% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index d5137a0e939d0..c88d468f1755a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -19,11 +19,10 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import com.android.settingslib.mobile.MobileMappings.Config -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { +class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _subscriptionsFlow = MutableStateFlow>(listOf()) override val subscriptionsFlow: Flow> = _subscriptionsFlow @@ -34,10 +33,9 @@ class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { private val _defaultDataSubRatConfig = MutableStateFlow(Config()) override val defaultDataSubRatConfig = _defaultDataSubRatConfig - private val subIdFlows = mutableMapOf>() - override fun getFlowForSubId(subId: Int): Flow { - return subIdFlows[subId] - ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it } + private val subIdRepos = mutableMapOf() + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } } fun setSubscriptions(subs: List) { @@ -52,8 +50,7 @@ class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { _activeMobileDataSubscriptionId.value = subId } - fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) { - val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet") - subscription.value = model + fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) { + subIdRepos[subId] = repo } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt similarity index 50% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 31203b3147eb1..775e6dbb5e195 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -22,13 +22,7 @@ import android.telephony.SignalStrength import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback -import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener -import android.telephony.TelephonyCallback.CarrierNetworkListener -import android.telephony.TelephonyCallback.DataActivityListener -import android.telephony.TelephonyCallback.DataConnectionStateListener -import android.telephony.TelephonyCallback.DisplayInfoListener import android.telephony.TelephonyCallback.ServiceStateListener -import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA import android.telephony.TelephonyManager @@ -36,70 +30,52 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers import org.mockito.Mock -import org.mockito.Mockito.verify +import org.mockito.Mockito import org.mockito.MockitoAnnotations @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -class MobileSubscriptionRepositoryTest : SysuiTestCase() { - private lateinit var underTest: MobileSubscriptionRepositoryImpl +class MobileConnectionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileConnectionRepositoryImpl @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher private val scope = CoroutineScope(IMMEDIATE) - private val mobileMappings = FakeMobileMappingsProxy() @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever( - broadcastDispatcher.broadcastFlow( - any(), - nullable(), - ArgumentMatchers.anyInt(), - nullable(), - ) - ) - .thenReturn(flowOf(Unit)) + whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) underTest = - MobileSubscriptionRepositoryImpl( - subscriptionManager, + MobileConnectionRepositoryImpl( + SUB_1_ID, telephonyManager, - logger, - broadcastDispatcher, - context, - mobileMappings, IMMEDIATE, + logger, scope, ) } @@ -109,79 +85,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { scope.cancel() } - @Test - fun testSubscriptions_initiallyEmpty() = - runBlocking(IMMEDIATE) { - assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf()) - } - - @Test - fun testSubscriptions_listUpdates() = - runBlocking(IMMEDIATE) { - var latest: List? = null - - val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) - - job.cancel() - } - - @Test - fun testSubscriptions_removingSub_updatesList() = - runBlocking(IMMEDIATE) { - var latest: List? = null - - val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) - - // WHEN 2 networks show up - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // WHEN one network is removed - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // THEN the subscriptions list represents the newest change - assertThat(latest).isEqualTo(listOf(SUB_2)) - - job.cancel() - } - - @Test - fun testActiveDataSubscriptionId_initialValueIsInvalidId() = - runBlocking(IMMEDIATE) { - assertThat(underTest.activeMobileDataSubscriptionId.value) - .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID) - } - - @Test - fun testActiveDataSubscriptionId_updates() = - runBlocking(IMMEDIATE) { - var active: Int? = null - - val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) - - getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID) - - assertThat(active).isEqualTo(SUB_2_ID) - - job.cancel() - } - @Test fun testFlowForSubId_default() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) assertThat(latest).isEqualTo(MobileSubscriptionModel()) @@ -191,10 +99,8 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_emergencyOnly() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.isEmergencyOnly = true @@ -209,10 +115,8 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_emergencyOnly_toggles() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType() val serviceState = ServiceState() @@ -229,13 +133,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_signalStrengths_levelsUpdate() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() - val strength = signalStrength(1, 2, true) + val callback = getTelephonyCallbackForType() + val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) callback.onSignalStrengthsChanged(strength) assertThat(latest?.isGsm).isEqualTo(true) @@ -248,12 +150,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_dataConnectionState() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() + val callback = + getTelephonyCallbackForType() callback.onDataConnectionStateChanged(100, 200 /* unused */) assertThat(latest?.dataConnectionState).isEqualTo(100) @@ -264,12 +165,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_dataActivity() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() + val callback = getTelephonyCallbackForType() callback.onDataActivity(3) assertThat(latest?.dataActivityDirection).isEqualTo(3) @@ -280,12 +179,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_carrierNetworkChange() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() + val callback = getTelephonyCallbackForType() callback.onCarrierNetworkChange(true) assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) @@ -294,12 +191,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_defaultNetworkType() = + fun subscriptionFlow_networkType_default() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) val type = NETWORK_TYPE_UNKNOWN val expected = DefaultNetworkType(type) @@ -310,14 +205,12 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_networkTypeUpdates_default() = + fun subscriptionFlow_networkType_updatesUsingDefault() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() + val callback = getTelephonyCallbackForType() val type = NETWORK_TYPE_LTE val expected = DefaultNetworkType(type) val ti = mock().also { whenever(it.networkType).thenReturn(type) } @@ -329,14 +222,12 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_networkTypeUpdates_override() = + fun subscriptionFlow_networkType_updatesUsingOverride() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() + val callback = getTelephonyCallbackForType() val type = OVERRIDE_NETWORK_TYPE_LTE_CA val expected = OverrideNetworkType(type) val ti = @@ -350,49 +241,9 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { job.cancel() } - @Test - fun testFlowForSubId_isCached() = - runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - - val state1 = underTest.getFlowForSubId(SUB_1_ID) - val state2 = underTest.getFlowForSubId(SUB_1_ID) - - assertThat(state1).isEqualTo(state2) - } - - @Test - fun testFlowForSubId_isRemovedAfterFinish() = - runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - - var latest: MobileSubscriptionModel? = null - - // Start collecting on some flow - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) - - // There should be once cached flow now - assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1) - - // When the job is canceled, the cache should be cleared - job.cancel() - - assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0) - } - - private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { - val callbackCaptor = argumentCaptor() - verify(subscriptionManager) - .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) - return callbackCaptor.value!! - } - - private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener = - getTelephonyCallbackForType() - private fun getTelephonyCallbacks(): List { val callbackCaptor = argumentCaptor() - verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) return callbackCaptor.allValues } @@ -420,9 +271,5 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - - private const val SUB_2_ID = 2 - private val SUB_2 = - mock().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt new file mode 100644 index 0000000000000..326e0d28166f5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2022 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.repository + +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileConnectionsRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileConnectionsRepositoryImpl + + @Mock private lateinit var subscriptionManager: SubscriptionManager + @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + ArgumentMatchers.anyInt(), + nullable(), + ) + ) + .thenReturn(flowOf(Unit)) + + underTest = + MobileConnectionsRepositoryImpl( + subscriptionManager, + telephonyManager, + logger, + broadcastDispatcher, + context, + IMMEDIATE, + scope, + mock(), + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testSubscriptions_initiallyEmpty() = + runBlocking(IMMEDIATE) { + assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf()) + } + + @Test + fun testSubscriptions_listUpdates() = + runBlocking(IMMEDIATE) { + var latest: List? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + @Test + fun testSubscriptions_removingSub_updatesList() = + runBlocking(IMMEDIATE) { + var latest: List? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + // WHEN 2 networks show up + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // WHEN one network is removed + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // THEN the subscriptions list represents the newest change + assertThat(latest).isEqualTo(listOf(SUB_2)) + + job.cancel() + } + + @Test + fun testActiveDataSubscriptionId_initialValueIsInvalidId() = + runBlocking(IMMEDIATE) { + assertThat(underTest.activeMobileDataSubscriptionId.value) + .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + + @Test + fun testActiveDataSubscriptionId_updates() = + runBlocking(IMMEDIATE) { + var active: Int? = null + + val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) + + getTelephonyCallbackForType() + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + + assertThat(active).isEqualTo(SUB_2_ID) + + job.cancel() + } + + @Test + fun testConnectionRepository_validSubId_isCached() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1).isSameInstanceAs(repo2) + + job.cancel() + } + + @Test + fun testConnectionCache_clearsInvalidSubscriptions() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) + + // SUB_2 disappears + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + + job.cancel() + } + + @Test + fun testConnectionRepository_invalidSubId_throws() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + assertThrows(IllegalArgumentException::class.java) { + underTest.getRepoForSubId(SUB_1_ID) + } + + job.cancel() + } + + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { + val callbackCaptor = argumentCaptor() + verify(subscriptionManager) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun getTelephonyCallbacks(): List { + val callbackCaptor = argumentCaptor() + verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + private inline fun getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacks().filterIsInstance() + assertThat(cbs.size).isEqualTo(1) + return cbs[0] + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + private val SUB_1 = + mock().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 99a7f6a2af7f1..ff44af4c92040 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -26,7 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G @@ -45,10 +45,9 @@ import org.junit.Test @SmallTest class MobileIconInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconInteractor - private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy() private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) - private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID) + private val connectionRepository = FakeMobileConnectionRepository() @Before fun setUp() { @@ -57,16 +56,15 @@ class MobileIconInteractorTest : SysuiTestCase() { mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, mobileMappingsProxy, - sub1Flow, + connectionRepository, ) } @Test fun gsm_level_default_unknown() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(isGsm = true), - SUB_1_ID ) var latest: Int? = null @@ -80,13 +78,12 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_usesGsmLevel() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel( isGsm = true, primaryLevel = GSM_LEVEL, cdmaLevel = CDMA_LEVEL ), - SUB_1_ID ) var latest: Int? = null @@ -100,9 +97,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun cdma_level_default_unknown() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(isGsm = false), - SUB_1_ID ) var latest: Int? = null @@ -115,13 +111,12 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun cdma_usesCdmaLevel() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel( isGsm = false, primaryLevel = GSM_LEVEL, cdmaLevel = CDMA_LEVEL ), - SUB_1_ID ) var latest: Int? = null @@ -135,9 +130,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_three_g() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), - SUB_1_ID ) var latest: MobileIconGroup? = null @@ -151,19 +145,17 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_updates_on_change() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), - SUB_1_ID ) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel( resolvedNetworkType = DefaultNetworkType(FOUR_G), ), - SUB_1_ID ) yield() @@ -175,9 +167,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_5g_override_type() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)), - SUB_1_ID ) var latest: MobileIconGroup? = null @@ -191,11 +182,10 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_default_if_no_lookup() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel( resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), ), - SUB_1_ID ) var latest: MobileIconGroup? = null @@ -215,9 +205,5 @@ class MobileIconInteractorTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - - private const val SUB_2_ID = 2 - private val SUB_2 = - mock().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } } } 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 ff8c1e2862a42..b01efd18971f6 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 @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.SubscriptionInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker @@ -41,7 +41,7 @@ import org.mockito.MockitoAnnotations class MobileIconsInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconsInteractor private val userSetupRepository = FakeUserSetupRepository() - private val subscriptionsRepository = FakeMobileSubscriptionRepository() + private val subscriptionsRepository = FakeMobileConnectionsRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy() private val scope = CoroutineScope(IMMEDIATE)