diff options
| author | 2022-10-25 16:01:51 +0000 | |
|---|---|---|
| committer | 2022-10-25 16:01:51 +0000 | |
| commit | f0d3add0dc95f0a5530f08cd47cee78d4381ca1c (patch) | |
| tree | 79f2d687fe48120f44625d3157707cf18228d66e | |
| parent | baa22308216b0159c78e20d92add5cfdea46a556 (diff) | |
| parent | 690d0acc03e31589539ef43bf6f31018d7b67865 (diff) | |
Merge changes Id53733ac,I38e917cf into tm-qpr-dev am: ac6d616456 am: 690d0acc03
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20167270
Change-Id: Ic859635654c47c43e12774f3ecb8c2a850aa93c6
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
19 files changed, 1073 insertions, 306 deletions
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 2aaa085645e4..fcd1b8abefe4 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,10 +18,14 @@ 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 +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository @@ -41,10 +45,16 @@ 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 + + @Binds + abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy + + @Binds + abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt index 46ccf32cc7f9..eaba0e93e750 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt @@ -27,6 +27,7 @@ import android.telephony.TelephonyCallback.ServiceStateListener import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN /** * Data class containing all of the relevant information for a particular line of service, known as @@ -57,6 +58,11 @@ data class MobileSubscriptionModel( /** From [CarrierNetworkListener.onCarrierNetworkChange] */ val carrierNetworkChangeActive: Boolean? = null, - /** From [DisplayInfoListener.onDisplayInfoChanged] */ - val displayInfo: TelephonyDisplayInfo? = null + /** + * From [DisplayInfoListener.onDisplayInfoChanged]. + * + * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or + * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon + */ + val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt new file mode 100644 index 000000000000..f385806c1b22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -0,0 +1,33 @@ +/* + * 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.model + +import android.telephony.Annotation.NetworkType +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy + +/** + * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending + * on whether or not the display info contains an override type, we may have to call different + * methods on [MobileMappingsProxy] to generate an icon lookup key. + */ +sealed interface ResolvedNetworkType { + @NetworkType val type: Int +} + +data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType + +data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType 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/MobileConnectionRepository.kt index 36de2a254160..45284cf0332b 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/MobileConnectionRepository.kt @@ -21,23 +21,18 @@ 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.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -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.shared.ConnectivityPipelineLogger +import java.lang.IllegalStateException import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -47,110 +42,64 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext /** - * Repo for monitoring the complete active subscription info list, to be consumed and filtered based - * on various policy + * 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 MobileSubscriptionRepository { - /** Observable list of current mobile subscriptions */ - val subscriptionsFlow: Flow<List<SubscriptionInfo>> - - /** Observable for the subscriptionId of the current mobile data connection */ - val activeMobileDataSubscriptionId: Flow<Int> - - /** Get or create an observable for the given subscription ID */ - fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> +interface MobileConnectionRepository { + /** + * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single + * listener + model. + */ + val subscriptionModelFlow: Flow<MobileSubscriptionModel> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) -@SysUISingleton -class MobileSubscriptionRepositoryImpl -@Inject -constructor( - private val subscriptionManager: SubscriptionManager, - private val telephonyManager: TelephonyManager, - @Background private val bgDispatcher: CoroutineDispatcher, - @Application private val scope: CoroutineScope, -) : MobileSubscriptionRepository { - private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf() - - /** - * State flow that emits the set of mobile data subscriptions, each represented by its own - * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each - * info object, but for now we keep track of the infos themselves. - */ - override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = - conflatedCallbackFlow { - val callback = - object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - bgDispatcher.asExecutor(), - callback, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } - } - .mapLatest { fetchSubscriptionsList() } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) - - /** StateFlow that keeps track of the current active mobile data subscription */ - override val activeMobileDataSubscriptionId: StateFlow<Int> = - conflatedCallbackFlow { - val callback = - object : TelephonyCallback(), ActiveDataSubscriptionIdListener { - override fun onActiveDataSubscriptionIdChanged(subId: Int) { - trySend(subId) - } - } - - telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) - awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } - } - .stateIn( - scope, - started = SharingStarted.WhileSubscribed(), - SubscriptionManager.INVALID_SUBSCRIPTION_ID +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." ) - - /** - * 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<MobileSubscriptionModel> { - return subIdFlowCache[subId] - ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it } + } } - @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache - - private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run { + override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = 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 { + 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 @@ -173,6 +122,7 @@ constructor( ) trySend(state) } + override fun onDataConnectionStateChanged( dataState: Int, networkType: Int @@ -180,31 +130,56 @@ constructor( 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 ) { - state = state.copy(displayInfo = 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) - } + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) } .stateIn(scope, SharingStarted.WhileSubscribed(), state) } - private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = - withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } + 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/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt new file mode 100644 index 000000000000..0e2428ae393a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -0,0 +1,201 @@ +/* + * 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.content.Context +import android.content.IntentFilter +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.annotation.VisibleForTesting +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +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.shared.ConnectivityPipelineLogger +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.combine +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Repo for monitoring the complete active subscription info list, to be consumed and filtered based + * on various policy + */ +interface MobileConnectionsRepository { + /** Observable list of current mobile subscriptions */ + val subscriptionsFlow: Flow<List<SubscriptionInfo>> + + /** Observable for the subscriptionId of the current mobile data connection */ + val activeMobileDataSubscriptionId: Flow<Int> + + /** Observable for [MobileMappings.Config] tracking the defaults */ + val defaultDataSubRatConfig: StateFlow<Config> + + /** 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 MobileConnectionsRepositoryImpl +@Inject +constructor( + private val subscriptionManager: SubscriptionManager, + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + broadcastDispatcher: BroadcastDispatcher, + private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory +) : MobileConnectionsRepository { + private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() + + /** + * State flow that emits the set of mobile data subscriptions, each represented by its own + * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each + * info object, but for now we keep track of the infos themselves. + */ + override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = + conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + 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 */ + override val activeMobileDataSubscriptionId: StateFlow<Int> = + conflatedCallbackFlow { + val callback = + object : TelephonyCallback(), ActiveDataSubscriptionIdListener { + override fun onActiveDataSubscriptionIdChanged(subId: Int) { + trySend(subId) + } + } + + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ) + + private val defaultDataSubChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) + + private val carrierConfigChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) + ) + + /** + * [Config] is an object that tracks relevant configuration flags for a given subscription ID. + * In the case of [MobileMappings], it's hard-coded to check the default data subscription's + * config, so this will apply to every icon that we care about. + * + * Relevant bits in the config are things like + * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL] + * + * This flow will produce whenever the default data subscription or the carrier config changes. + */ + override val defaultDataSubRatConfig: StateFlow<Config> = + combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ -> + Config.readConfig(context) + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + initialValue = Config.readConfig(context) + ) + + 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 } + } + + private fun isValidSubId(subId: Int): Boolean { + subscriptionsFlow.value.forEach { + if (it.subscriptionId == subId) { + return true + } + } + + return false + } + + @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + + private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { + return mobileConnectionRepositoryFactory.build(subId) + } + + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { + // 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<SubscriptionInfo> = + withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } +} 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 40fe0f3e8fe0..15f4acc1127c 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 @@ -17,32 +17,58 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager -import com.android.settingslib.SignalIcon -import com.android.settingslib.mobile.TelephonyIcons -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +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.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 +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map interface MobileIconInteractor { - /** Identifier for RAT type indicator */ - val iconGroup: Flow<SignalIcon.MobileIconGroup> + /** Observable for RAT type (network type) indicator */ + val networkTypeIconGroup: Flow<MobileIconGroup> + /** True if this line of service is emergency-only */ val isEmergencyOnly: Flow<Boolean> + /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ val level: Flow<Int> + /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ val numberOfLevels: Flow<Int> + /** True when we want to draw an icon that makes room for the exclamation mark */ val cutOut: Flow<Boolean> } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ class MobileIconInteractorImpl( - mobileStatusInfo: Flow<MobileSubscriptionModel>, + defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>, + defaultMobileIconGroup: Flow<MobileIconGroup>, + mobileMappingsProxy: MobileMappingsProxy, + connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { - override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G) + private val mobileStatusInfo = connectionRepository.subscriptionModelFlow + + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ + override val networkTypeIconGroup: Flow<MobileIconGroup> = + combine( + mobileStatusInfo, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { info, mapping, defaultGroup -> + val lookupKey = + when (val resolved = info.resolvedNetworkType) { + is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) + is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type) + } + mapping[lookupKey] ?: defaultGroup + } + override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly } override val level: Flow<Int> = 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 8e67e19f3e35..cd411a4a2afe 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 @@ -19,29 +19,51 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository +import com.android.systemui.dagger.qualifiers.Application +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 import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** - * Business layer logic for mobile subscription icons + * Business layer logic for the set of mobile subscription icons. * - * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s - * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs + * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]). + * The list of subscriptions is filtered based on the opportunistic flags on the infos. + * + * It provides the default mapping between the telephony display info and the icon group that + * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual + * icon */ +interface MobileIconsInteractor { + val filteredSubscriptions: Flow<List<SubscriptionInfo>> + val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> + val defaultMobileIconGroup: Flow<MobileIconGroup> + val isUserSetup: Flow<Boolean> + fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor +} + @SysUISingleton -class MobileIconsInteractor +class MobileIconsInteractorImpl @Inject constructor( - private val mobileSubscriptionRepo: MobileSubscriptionRepository, + private val mobileSubscriptionRepo: MobileConnectionsRepository, private val carrierConfigTracker: CarrierConfigTracker, + private val mobileMappingsProxy: MobileMappingsProxy, userSetupRepo: UserSetupRepository, -) { + @Application private val scope: CoroutineScope, +) : MobileIconsInteractor { private val activeMobileDataSubscriptionId = mobileSubscriptionRepo.activeMobileDataSubscriptionId @@ -61,7 +83,7 @@ constructor( * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], * and by checking which subscription is opportunistic, or which one is active. */ - val filteredSubscriptions: Flow<List<SubscriptionInfo>> = + override val filteredSubscriptions: Flow<List<SubscriptionInfo>> = combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId -> // Based on the old logic, @@ -92,15 +114,29 @@ constructor( } } - val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow - - /** Vends out new [MobileIconInteractor] for a particular subId */ - fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = - MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId)) - /** - * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections + * Mapping from network type to [MobileIconGroup] using the config generated for the default + * subscription Id. This mapping is the same for every subscription. */ - private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> = - mobileSubscriptionRepo.getFlowForSubId(subId) + override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = + mobileSubscriptionRepo.defaultDataSubRatConfig + .map { mobileMappingsProxy.mapIconSets(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) + + /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ + override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = + mobileSubscriptionRepo.defaultDataSubRatConfig + .map { mobileMappingsProxy.getDefaultIcons(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) + + override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow + + /** Vends out new [MobileIconInteractor] for a particular subId */ + override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = + MobileIconInteractorImpl( + defaultMobileIconMapping, + defaultMobileIconGroup, + mobileMappingsProxy, + mobileSubscriptionRepo.getRepoForSubId(subId), + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 1405b050234b..67ea139271fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import android.content.res.ColorStateList +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageView import androidx.core.view.isVisible @@ -24,6 +26,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.settingslib.graph.SignalDrawable import com.android.systemui.R +import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel import kotlinx.coroutines.flow.collect @@ -37,6 +40,7 @@ object MobileIconBinder { view: ViewGroup, viewModel: MobileIconViewModel, ) { + val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type) val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } @@ -52,10 +56,20 @@ object MobileIconBinder { } } + // Set the network type icon + launch { + viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId -> + dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) } + networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE + } + } + // Set the tint launch { viewModel.tint.collect { tint -> - iconView.imageTintList = ColorStateList.valueOf(tint) + val tintList = ColorStateList.valueOf(tint) + iconView.imageTintList = tintList + networkTypeView.imageTintList = tintList } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index cfabeba8432c..cc8f6dd08585 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger @@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -54,5 +57,15 @@ constructor( .distinctUntilChanged() .logOutputChange(logger, "iconId($subscriptionId)") + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + var networkTypeIcon: Flow<Icon?> = + iconInteractor.networkTypeIconGroup.map { + val desc = + if (it.dataContentDescription != 0) + ContentDescription.Resource(it.dataContentDescription) + else null + Icon.Resource(it.dataType, desc) + } + var tint: Flow<Int> = flowOf(Color.CYAN) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt new file mode 100644 index 000000000000..60bd0383f8c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt @@ -0,0 +1,52 @@ +/* + * 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.util + +import android.telephony.Annotation.NetworkType +import android.telephony.TelephonyDisplayInfo +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import javax.inject.Inject + +/** + * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to + * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This + * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake + * them out in tests + */ +interface MobileMappingsProxy { + fun mapIconSets(config: Config): Map<String, MobileIconGroup> + fun getDefaultIcons(config: Config): MobileIconGroup + fun toIconKey(@NetworkType networkType: Int): String + fun toIconKeyOverride(@NetworkType networkType: Int): String +} + +/** Injectable wrapper class for [MobileMappings] */ +class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy { + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = + MobileMappings.mapIconSets(config) + + override fun getDefaultIcons(config: Config): MobileIconGroup = + MobileMappings.getDefaultIcons(config) + + override fun toIconKey(@NetworkType networkType: Int): String = + MobileMappings.toIconKey(networkType) + + override fun toIconKeyOverride(networkType: Int): String = + MobileMappings.toDisplayIconKey(networkType) +} 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 000000000000..6ff7b7ccd5e3 --- /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<MobileSubscriptionModel> = _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 index 0d1526883023..c88d468f1755 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 @@ -18,11 +18,11 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.settingslib.mobile.MobileMappings.Config import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { +class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow @@ -30,22 +30,27 @@ class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId - private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>() - override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> { - return subIdFlows[subId] - ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it } + private val _defaultDataSubRatConfig = MutableStateFlow(Config()) + override val defaultDataSubRatConfig = _defaultDataSubRatConfig + + private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } } fun setSubscriptions(subs: List<SubscriptionInfo>) { _subscriptionsFlow.value = subs } + fun setDefaultDataSubRatConfig(config: Config) { + _defaultDataSubRatConfig.value = config + } + fun setActiveMobileDataSubscriptionId(subId: Int) { _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 index 316b795ac949..775e6dbb5e19 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,18 +22,18 @@ 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 +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.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 com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock @@ -50,28 +50,32 @@ import org.junit.After import org.junit.Before import org.junit.Test 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 + private val scope = CoroutineScope(IMMEDIATE) @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) underTest = - MobileSubscriptionRepositoryImpl( - subscriptionManager, + MobileConnectionRepositoryImpl( + SUB_1_ID, telephonyManager, IMMEDIATE, + logger, scope, ) } @@ -82,78 +86,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testSubscriptions_initiallyEmpty() = - runBlocking(IMMEDIATE) { - assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>()) - } - - @Test - fun testSubscriptions_listUpdates() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionInfo>? = 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<SubscriptionInfo>? = 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()) @@ -163,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 @@ -181,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<ServiceStateListener>() val serviceState = ServiceState() @@ -201,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<SignalStrengthsListener>() - val strength = signalStrength(1, 2, true) + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) callback.onSignalStrengthsChanged(strength) assertThat(latest?.isGsm).isEqualTo(true) @@ -220,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<DataConnectionStateListener>() + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(100, 200 /* unused */) assertThat(latest?.dataConnectionState).isEqualTo(100) @@ -236,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<DataActivityListener>() + val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>() callback.onDataActivity(3) assertThat(latest?.dataActivityDirection).isEqualTo(3) @@ -252,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<CarrierNetworkListener>() + val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() callback.onCarrierNetworkChange(true) assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) @@ -266,65 +191,59 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_displayInfo() = + 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 callback = getTelephonyCallbackForType<DisplayInfoListener>() - val ti = mock<TelephonyDisplayInfo>() - callback.onDisplayInfoChanged(ti) + val type = NETWORK_TYPE_UNKNOWN + val expected = DefaultNetworkType(type) - assertThat(latest?.displayInfo).isEqualTo(ti) + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) job.cancel() } @Test - fun testFlowForSubId_isCached() = + fun subscriptionFlow_networkType_updatesUsingDefault() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val state1 = underTest.getFlowForSubId(SUB_1_ID) - val state2 = underTest.getFlowForSubId(SUB_1_ID) + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = NETWORK_TYPE_LTE + val expected = DefaultNetworkType(type) + val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } + callback.onDisplayInfoChanged(ti) - assertThat(state1).isEqualTo(state2) + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + + job.cancel() } @Test - fun testFlowForSubId_isRemovedAfterFinish() = + fun subscriptionFlow_networkType_updatesUsingOverride() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = OVERRIDE_NETWORK_TYPE_LTE_CA + val expected = OverrideNetworkType(type) + val ti = + mock<TelephonyDisplayInfo>().also { + whenever(it.overrideNetworkType).thenReturn(type) + } + callback.onDisplayInfoChanged(ti) - // 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) + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) - // 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<SubscriptionManager.OnSubscriptionsChangedListener>() - verify(subscriptionManager) - .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) - return callbackCaptor.value!! - } - - private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener = - getTelephonyCallbackForType() - private fun getTelephonyCallbacks(): List<TelephonyCallback> { val callbackCaptor = argumentCaptor<TelephonyCallback>() - verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) return callbackCaptor.allValues } @@ -352,9 +271,5 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - - private const val SUB_2_ID = 2 - private val SUB_2 = - mock<SubscriptionInfo>().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 000000000000..326e0d28166f --- /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<SubscriptionInfo>()) + } + + @Test + fun testSubscriptions_listUpdates() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = 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<SubscriptionInfo>? = 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<ActiveDataSubscriptionIdListener>() + .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<SubscriptionManager.OnSubscriptionsChangedListener>() + verify(subscriptionManager) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun getTelephonyCallbacks(): List<TelephonyCallback> { + val callbackCaptor = argumentCaptor<TelephonyCallback>() + verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + private inline fun <reified T> getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacks().filterIsInstance<T>() + 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<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 8ec68f36a837..cd4dbebcc35c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileIconInteractor : MobileIconInteractor { private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN) - override val iconGroup = _iconGroup + override val networkTypeIconGroup = _iconGroup private val _isEmergencyOnly = MutableStateFlow<Boolean>(false) override val isEmergencyOnly = _isEmergencyOnly diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt new file mode 100644 index 000000000000..2bd228603cb0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -0,0 +1,75 @@ +/* + * 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.domain.interactor + +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO +import android.telephony.TelephonyManager.NETWORK_TYPE_GSM +import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) : + MobileIconsInteractor { + val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) + val LTE_KEY = mobileMappings.toIconKey(LTE) + val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) + val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE) + + /** + * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to + * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for + * the exhaustive set of icons + */ + val TEST_MAPPING: Map<String, MobileIconGroup> = + mapOf( + THREE_G_KEY to TelephonyIcons.THREE_G, + LTE_KEY to TelephonyIcons.LTE, + FOUR_G_KEY to TelephonyIcons.FOUR_G, + FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G, + ) + + private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf()) + override val filteredSubscriptions = _filteredSubscriptions + + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) + override val defaultMobileIconMapping = _defaultMobileIconMapping + + private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON) + override val defaultMobileIconGroup = _defaultMobileIconGroup + + private val _isUserSetup = MutableStateFlow(true) + override val isUserSetup = _isUserSetup + + /** Always returns a new fake interactor */ + override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { + return FakeMobileIconInteractor() + } + + companion object { + val DEFAULT_ICON = TelephonyIcons.G + + // Use [MobileMappings] to define some simple definitions + const val THREE_G = NETWORK_TYPE_GSM + const val LTE = NETWORK_TYPE_LTE + const val FOUR_G = NETWORK_TYPE_UMTS + const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO + } +} 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 2f07d9cb3831..ff44af4c9204 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 @@ -18,10 +18,19 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CellSignalStrength import android.telephony.SubscriptionInfo +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons 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.repository.FakeMobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +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 +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -29,26 +38,33 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @SmallTest class MobileIconInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconInteractor - private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository() - private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID) + private val mobileMappingsProxy = FakeMobileMappingsProxy() + private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) + private val connectionRepository = FakeMobileConnectionRepository() @Before fun setUp() { - underTest = MobileIconInteractorImpl(sub1Flow) + underTest = + MobileIconInteractorImpl( + mobileIconsInteractor.defaultMobileIconMapping, + mobileIconsInteractor.defaultMobileIconGroup, + mobileMappingsProxy, + connectionRepository, + ) } @Test fun gsm_level_default_unknown() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(isGsm = true), - SUB_1_ID ) var latest: Int? = null @@ -62,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 @@ -82,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 @@ -97,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 @@ -114,6 +127,75 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun iconGroup_three_g() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.THREE_G) + + job.cancel() + } + + @Test + fun iconGroup_updates_on_change() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + resolvedNetworkType = DefaultNetworkType(FOUR_G), + ), + ) + yield() + + assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G) + + job.cancel() + } + + @Test + fun iconGroup_5g_override_type() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.NR_5G) + + job.cancel() + } + + @Test + fun iconGroup_default_if_no_lookup() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON) + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate @@ -123,9 +205,5 @@ class MobileIconInteractorTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - - private const val SUB_2_ID = 2 - private val SUB_2 = - mock<SubscriptionInfo>().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 89ad9cb9e51e..b01efd18971f 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,12 +19,14 @@ 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 import com.android.systemui.util.mockito.mock 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.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -39,7 +41,9 @@ 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) @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker @@ -47,10 +51,12 @@ class MobileIconsInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) underTest = - MobileIconsInteractor( + MobileIconsInteractorImpl( subscriptionsRepository, carrierConfigTracker, + mobileMappingsProxy, userSetupRepository, + scope ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt new file mode 100644 index 000000000000..6d8d902615de --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt @@ -0,0 +1,46 @@ +/* + * 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.util + +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.settingslib.mobile.TelephonyIcons + +class FakeMobileMappingsProxy : MobileMappingsProxy { + private var iconMap = mapOf<String, MobileIconGroup>() + private var defaultIcons = TelephonyIcons.THREE_G + + fun setIconMap(map: Map<String, MobileIconGroup>) { + iconMap = map + } + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap + fun getIconMap() = iconMap + + fun setDefaultIcons(group: MobileIconGroup) { + defaultIcons = group + } + override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons + fun getDefaultIcons(): MobileIconGroup = defaultIcons + + override fun toIconKey(networkType: Int): String { + return networkType.toString() + } + + override fun toIconKeyOverride(networkType: Int): String { + return toIconKey(networkType) + "_override" + } +} |