Merge changes Id53733ac,I38e917cf into tm-qpr-dev

* changes:
  [Sb refactor] Move subIdFlow into its own repository
  [Status bar refactor] Implement the lookup from network type to RAT icon
This commit is contained in:
Evan Laird
2022-10-25 15:18:58 +00:00
committed by Android (Google) Code Review
19 changed files with 1077 additions and 310 deletions

View File

@@ -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
}

View File

@@ -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),
)

View File

@@ -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

View File

@@ -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,
)
}
}
}

View File

@@ -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 }
}

View File

@@ -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> =

View File

@@ -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
/**
* Mapping from network type to [MobileIconGroup] using the config generated for the default
* subscription Id. This mapping is the same for every subscription.
*/
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 */
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
*/
private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
mobileSubscriptionRepo.getFlowForSubId(subId)
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
MobileIconInteractorImpl(
defaultMobileIconMapping,
defaultMobileIconGroup,
mobileMappingsProxy,
mobileSubscriptionRepo.getRepoForSubId(subId),
)
}

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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,
)
}
@@ -81,79 +85,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
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)
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>()
val type = NETWORK_TYPE_UNKNOWN
val expected = DefaultNetworkType(type)
assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
job.cancel()
}
@Test
fun subscriptionFlow_networkType_updatesUsingDefault() =
runBlocking(IMMEDIATE) {
var latest: MobileSubscriptionModel? = null
val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
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(latest?.displayInfo).isEqualTo(ti)
assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
job.cancel()
}
@Test
fun testFlowForSubId_isCached() =
fun subscriptionFlow_networkType_updatesUsingOverride() =
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
val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
// Start collecting on some flow
val job = underTest.getFlowForSubId(SUB_1_ID).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)
// 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) }
}
}

View File

@@ -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) }
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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) }
}
}

View File

@@ -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
)
}

View File

@@ -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"
}
}