From e6fd49588f42e0bbd308d5bc599a662b4f2116eb Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Tue, 11 Oct 2022 18:03:55 -0400 Subject: [PATCH] [Sb refactor] Move subIdFlow into its own repository This CL keeps the pattern of "MobileIcons/MobileIcon" to distinguish 2 layers of MobileConnection repositories here: `MobileConnectionsRepository` and `MobileConnectionRepository`. This allows us to use ad-hoc repository classes to track state from `TelephonyManager` objects created using `TelephonyManager#createForSubscriptionId`. The intention now is that the top-level repository will track and cache the child repos as needed, and remove its cache when the subscription list changes such that they no longer track valid subIds. Downstream observers will still have to be cleaned up via the UiAdapter, which will close the observed data streams. Test: atest MobileConnectionRepositoryTest Bug: 240492102 Change-Id: Id53733ac8a84869a57133b2e0055105a716a219d --- .../dagger/StatusBarPipelineModule.kt | 10 +- .../repository/MobileConnectionRepository.kt | 185 +++++++++++++ ...tory.kt => MobileConnectionsRepository.kt} | 159 +++-------- .../domain/interactor/MobileIconInteractor.kt | 6 +- .../interactor/MobileIconsInteractor.kt | 13 +- .../FakeMobileConnectionRepository.kt | 30 +++ ....kt => FakeMobileConnectionsRepository.kt} | 15 +- ...t.kt => MobileConnectionRepositoryTest.kt} | 211 +++------------ .../MobileConnectionsRepositoryTest.kt | 246 ++++++++++++++++++ .../interactor/MobileIconInteractorTest.kt | 38 +-- .../interactor/MobileIconsInteractorTest.kt | 4 +- 11 files changed, 564 insertions(+), 353 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt rename packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/{MobileSubscriptionRepository.kt => MobileConnectionsRepository.kt} (51%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt rename packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/{FakeMobileSubscriptionRepository.kt => FakeMobileConnectionsRepository.kt} (72%) rename packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/{MobileSubscriptionRepositoryTest.kt => MobileConnectionRepositoryTest.kt} (50%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt 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)