Merge changes Id53733ac,I38e917cf into tm-qpr-dev am: ac6d616456

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20167270

Change-Id: I90e216a51dd7691fcfbc14ab5b49f7353ca4e50c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Evan Laird
2022-10-25 15:24:23 +00:00
committed by Automerger Merge Worker
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.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl 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.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl 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.UserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl 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.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -41,10 +45,16 @@ abstract class StatusBarPipelineModule {
abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
@Binds @Binds
abstract fun mobileSubscriptionRepository( abstract fun mobileConnectionsRepository(
impl: MobileSubscriptionRepositoryImpl impl: MobileConnectionsRepositoryImpl
): MobileSubscriptionRepository ): MobileConnectionsRepository
@Binds @Binds
abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository 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.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager 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 * 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] */ /** From [CarrierNetworkListener.onCarrierNetworkChange] */
val carrierNetworkChangeActive: Boolean? = null, 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.ServiceState
import android.telephony.SignalStrength import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback 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
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 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.Application
import com.android.systemui.dagger.qualifiers.Background 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.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 javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -47,110 +42,64 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
/** /**
* Repo for monitoring the complete active subscription info list, to be consumed and filtered based * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
* on various policy * 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 { interface MobileConnectionRepository {
/** Observable list of current mobile subscriptions */ /**
val subscriptionsFlow: Flow<List<SubscriptionInfo>> * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
* listener + model.
/** Observable for the subscriptionId of the current mobile data connection */ */
val activeMobileDataSubscriptionId: Flow<Int> val subscriptionModelFlow: Flow<MobileSubscriptionModel>
/** Get or create an observable for the given subscription ID */
fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
} }
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton class MobileConnectionRepositoryImpl(
class MobileSubscriptionRepositoryImpl private val subId: Int,
@Inject telephonyManager: TelephonyManager,
constructor( bgDispatcher: CoroutineDispatcher,
private val subscriptionManager: SubscriptionManager, logger: ConnectivityPipelineLogger,
private val telephonyManager: TelephonyManager, scope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher, ) : MobileConnectionRepository {
@Application private val scope: CoroutineScope, init {
) : MobileSubscriptionRepository { if (telephonyManager.subscriptionId != subId) {
private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf() throw IllegalStateException(
"TelephonyManager should be created with subId($subId). " +
/** "Found ${telephonyManager.subscriptionId} instead."
* 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) override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.stateIn(
scope,
started = SharingStarted.WhileSubscribed(),
SubscriptionManager.INVALID_SUBSCRIPTION_ID
)
/**
* 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 {
var state = MobileSubscriptionModel() var state = MobileSubscriptionModel()
conflatedCallbackFlow { conflatedCallbackFlow {
val phony = telephonyManager.createForSubscriptionId(subId)
// TODO (b/240569788): log all of these into the connectivity logger // TODO (b/240569788): log all of these into the connectivity logger
val callback = val callback =
object : object :
TelephonyCallback(), TelephonyCallback(),
ServiceStateListener, TelephonyCallback.ServiceStateListener,
SignalStrengthsListener, TelephonyCallback.SignalStrengthsListener,
DataConnectionStateListener, TelephonyCallback.DataConnectionStateListener,
DataActivityListener, TelephonyCallback.DataActivityListener,
CarrierNetworkListener, TelephonyCallback.CarrierNetworkListener,
DisplayInfoListener { TelephonyCallback.DisplayInfoListener {
override fun onServiceStateChanged(serviceState: ServiceState) { override fun onServiceStateChanged(serviceState: ServiceState) {
state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
trySend(state) trySend(state)
} }
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
val cdmaLevel = val cdmaLevel =
signalStrength signalStrength
@@ -173,6 +122,7 @@ constructor(
) )
trySend(state) trySend(state)
} }
override fun onDataConnectionStateChanged( override fun onDataConnectionStateChanged(
dataState: Int, dataState: Int,
networkType: Int networkType: Int
@@ -180,31 +130,56 @@ constructor(
state = state.copy(dataConnectionState = dataState) state = state.copy(dataConnectionState = dataState)
trySend(state) trySend(state)
} }
override fun onDataActivity(direction: Int) { override fun onDataActivity(direction: Int) {
state = state.copy(dataActivityDirection = direction) state = state.copy(dataActivityDirection = direction)
trySend(state) trySend(state)
} }
override fun onCarrierNetworkChange(active: Boolean) { override fun onCarrierNetworkChange(active: Boolean) {
state = state.copy(carrierNetworkChangeActive = active) state = state.copy(carrierNetworkChangeActive = active)
trySend(state) trySend(state)
} }
override fun onDisplayInfoChanged( override fun onDisplayInfoChanged(
telephonyDisplayInfo: TelephonyDisplayInfo 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) trySend(state)
} }
} }
phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
phony.unregisterTelephonyCallback(callback)
// Release the cached flow
subIdFlowCache.remove(subId)
}
} }
.onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) }
.stateIn(scope, SharingStarted.WhileSubscribed(), state) .stateIn(scope, SharingStarted.WhileSubscribed(), state)
} }
private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = class Factory
withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } @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 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.telephony.CarrierConfigManager import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.CarrierConfigTracker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
interface MobileIconInteractor { interface MobileIconInteractor {
/** Identifier for RAT type indicator */ /** Observable for RAT type (network type) indicator */
val iconGroup: Flow<SignalIcon.MobileIconGroup> val networkTypeIconGroup: Flow<MobileIconGroup>
/** True if this line of service is emergency-only */ /** True if this line of service is emergency-only */
val isEmergencyOnly: Flow<Boolean> val isEmergencyOnly: Flow<Boolean>
/** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
val level: Flow<Int> val level: Flow<Int>
/** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
val numberOfLevels: Flow<Int> val numberOfLevels: Flow<Int>
/** True when we want to draw an icon that makes room for the exclamation mark */ /** True when we want to draw an icon that makes room for the exclamation mark */
val cutOut: Flow<Boolean> val cutOut: Flow<Boolean>
} }
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
class MobileIconInteractorImpl( class MobileIconInteractorImpl(
mobileStatusInfo: Flow<MobileSubscriptionModel>, defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
defaultMobileIconGroup: Flow<MobileIconGroup>,
mobileMappingsProxy: MobileMappingsProxy,
connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor { ) : 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 isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
override val level: Flow<Int> = 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.CarrierConfigManager
import android.telephony.SubscriptionInfo import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager 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.dagger.SysUISingleton
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine 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 * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
* that the system knows about. They obey policy that depends on OEM, carrier, and locale configs * 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 @SysUISingleton
class MobileIconsInteractor class MobileIconsInteractorImpl
@Inject @Inject
constructor( constructor(
private val mobileSubscriptionRepo: MobileSubscriptionRepository, private val mobileSubscriptionRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker, private val carrierConfigTracker: CarrierConfigTracker,
private val mobileMappingsProxy: MobileMappingsProxy,
userSetupRepo: UserSetupRepository, userSetupRepo: UserSetupRepository,
) { @Application private val scope: CoroutineScope,
) : MobileIconsInteractor {
private val activeMobileDataSubscriptionId = private val activeMobileDataSubscriptionId =
mobileSubscriptionRepo.activeMobileDataSubscriptionId mobileSubscriptionRepo.activeMobileDataSubscriptionId
@@ -61,7 +83,7 @@ constructor(
* [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
* and by checking which subscription is opportunistic, or which one is active. * 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 combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
-> ->
// Based on the old logic, // 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 */ /** Vends out new [MobileIconInteractor] for a particular subId */
fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId)) MobileIconInteractorImpl(
defaultMobileIconMapping,
/** defaultMobileIconGroup,
* Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections mobileMappingsProxy,
*/ mobileSubscriptionRepo.getRepoForSubId(subId),
private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> = )
mobileSubscriptionRepo.getFlowForSubId(subId)
} }

View File

@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.binder package com.android.systemui.statusbar.pipeline.mobile.ui.binder
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -24,6 +26,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.graph.SignalDrawable import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@@ -37,6 +40,7 @@ object MobileIconBinder {
view: ViewGroup, view: ViewGroup,
viewModel: MobileIconViewModel, viewModel: MobileIconViewModel,
) { ) {
val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } 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 // Set the tint
launch { launch {
viewModel.tint.collect { tint -> 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 android.graphics.Color
import com.android.settingslib.graph.SignalDrawable 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.MobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger 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.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf 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 * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -54,5 +57,15 @@ constructor(
.distinctUntilChanged() .distinctUntilChanged()
.logOutputChange(logger, "iconId($subscriptionId)") .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) 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.SubscriptionInfo
import android.telephony.SubscriptionManager 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.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { class FakeMobileConnectionsRepository : MobileConnectionsRepository {
private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
@@ -30,22 +30,27 @@ class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>() private val _defaultDataSubRatConfig = MutableStateFlow(Config())
override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> { override val defaultDataSubRatConfig = _defaultDataSubRatConfig
return subIdFlows[subId]
?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it } 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>) { fun setSubscriptions(subs: List<SubscriptionInfo>) {
_subscriptionsFlow.value = subs _subscriptionsFlow.value = subs
} }
fun setDefaultDataSubRatConfig(config: Config) {
_defaultDataSubRatConfig.value = config
}
fun setActiveMobileDataSubscriptionId(subId: Int) { fun setActiveMobileDataSubscriptionId(subId: Int) {
_activeMobileDataSubscriptionId.value = subId _activeMobileDataSubscriptionId.value = subId
} }
fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) { fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) {
val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet") subIdRepos[subId] = repo
subscription.value = model
} }
} }

View File

@@ -22,18 +22,18 @@ import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback 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.ServiceStateListener
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase 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.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.any
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
@@ -50,28 +50,32 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify import org.mockito.Mockito
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
class MobileSubscriptionRepositoryTest : SysuiTestCase() { class MobileConnectionRepositoryTest : SysuiTestCase() {
private lateinit var underTest: MobileSubscriptionRepositoryImpl private lateinit var underTest: MobileConnectionRepositoryImpl
@Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
private val scope = CoroutineScope(IMMEDIATE) private val scope = CoroutineScope(IMMEDIATE)
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
underTest = underTest =
MobileSubscriptionRepositoryImpl( MobileConnectionRepositoryImpl(
subscriptionManager, SUB_1_ID,
telephonyManager, telephonyManager,
IMMEDIATE, IMMEDIATE,
logger,
scope, scope,
) )
} }
@@ -81,79 +85,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
scope.cancel() 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 @Test
fun testFlowForSubId_default() = fun testFlowForSubId_default() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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()) assertThat(latest).isEqualTo(MobileSubscriptionModel())
@@ -163,10 +99,8 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
@Test @Test
fun testFlowForSubId_emergencyOnly() = fun testFlowForSubId_emergencyOnly() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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() val serviceState = ServiceState()
serviceState.isEmergencyOnly = true serviceState.isEmergencyOnly = true
@@ -181,10 +115,8 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
@Test @Test
fun testFlowForSubId_emergencyOnly_toggles() = fun testFlowForSubId_emergencyOnly_toggles() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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 callback = getTelephonyCallbackForType<ServiceStateListener>()
val serviceState = ServiceState() val serviceState = ServiceState()
@@ -201,13 +133,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
@Test @Test
fun testFlowForSubId_signalStrengths_levelsUpdate() = fun testFlowForSubId_signalStrengths_levelsUpdate() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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 callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
val strength = signalStrength(1, 2, true) val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
callback.onSignalStrengthsChanged(strength) callback.onSignalStrengthsChanged(strength)
assertThat(latest?.isGsm).isEqualTo(true) assertThat(latest?.isGsm).isEqualTo(true)
@@ -220,12 +150,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
@Test @Test
fun testFlowForSubId_dataConnectionState() = fun testFlowForSubId_dataConnectionState() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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 */) callback.onDataConnectionStateChanged(100, 200 /* unused */)
assertThat(latest?.dataConnectionState).isEqualTo(100) assertThat(latest?.dataConnectionState).isEqualTo(100)
@@ -236,12 +165,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
@Test @Test
fun testFlowForSubId_dataActivity() = fun testFlowForSubId_dataActivity() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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) callback.onDataActivity(3)
assertThat(latest?.dataActivityDirection).isEqualTo(3) assertThat(latest?.dataActivityDirection).isEqualTo(3)
@@ -252,12 +179,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
@Test @Test
fun testFlowForSubId_carrierNetworkChange() = fun testFlowForSubId_carrierNetworkChange() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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) callback.onCarrierNetworkChange(true)
assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
@@ -266,65 +191,59 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
} }
@Test @Test
fun testFlowForSubId_displayInfo() = fun subscriptionFlow_networkType_default() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
var latest: MobileSubscriptionModel? = null 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 type = NETWORK_TYPE_UNKNOWN
val ti = mock<TelephonyDisplayInfo>() 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) callback.onDisplayInfoChanged(ti)
assertThat(latest?.displayInfo).isEqualTo(ti) assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
job.cancel() job.cancel()
} }
@Test @Test
fun testFlowForSubId_isCached() = fun subscriptionFlow_networkType_updatesUsingOverride() =
runBlocking(IMMEDIATE) { 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 var latest: MobileSubscriptionModel? = null
val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
// Start collecting on some flow val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) 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(latest?.resolvedNetworkType).isEqualTo(expected)
assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
// When the job is canceled, the cache should be cleared
job.cancel() 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> { private fun getTelephonyCallbacks(): List<TelephonyCallback> {
val callbackCaptor = argumentCaptor<TelephonyCallback>() val callbackCaptor = argumentCaptor<TelephonyCallback>()
verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
return callbackCaptor.allValues return callbackCaptor.allValues
} }
@@ -352,9 +271,5 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() {
private const val SUB_1_ID = 1 private const val SUB_1_ID = 1
private val SUB_1 = private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } 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 { class FakeMobileIconInteractor : MobileIconInteractor {
private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN) private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
override val iconGroup = _iconGroup override val networkTypeIconGroup = _iconGroup
private val _isEmergencyOnly = MutableStateFlow<Boolean>(false) private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
override val isEmergencyOnly = _isEmergencyOnly 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.CellSignalStrength
import android.telephony.SubscriptionInfo import android.telephony.SubscriptionInfo
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest 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.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.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.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -29,26 +38,33 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@SmallTest @SmallTest
class MobileIconInteractorTest : SysuiTestCase() { class MobileIconInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconInteractor private lateinit var underTest: MobileIconInteractor
private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy()
private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID) private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
private val connectionRepository = FakeMobileConnectionRepository()
@Before @Before
fun setUp() { fun setUp() {
underTest = MobileIconInteractorImpl(sub1Flow) underTest =
MobileIconInteractorImpl(
mobileIconsInteractor.defaultMobileIconMapping,
mobileIconsInteractor.defaultMobileIconGroup,
mobileMappingsProxy,
connectionRepository,
)
} }
@Test @Test
fun gsm_level_default_unknown() = fun gsm_level_default_unknown() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
mobileSubscriptionRepository.setMobileSubscriptionModel( connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel(isGsm = true), MobileSubscriptionModel(isGsm = true),
SUB_1_ID
) )
var latest: Int? = null var latest: Int? = null
@@ -62,13 +78,12 @@ class MobileIconInteractorTest : SysuiTestCase() {
@Test @Test
fun gsm_usesGsmLevel() = fun gsm_usesGsmLevel() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
mobileSubscriptionRepository.setMobileSubscriptionModel( connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel( MobileSubscriptionModel(
isGsm = true, isGsm = true,
primaryLevel = GSM_LEVEL, primaryLevel = GSM_LEVEL,
cdmaLevel = CDMA_LEVEL cdmaLevel = CDMA_LEVEL
), ),
SUB_1_ID
) )
var latest: Int? = null var latest: Int? = null
@@ -82,9 +97,8 @@ class MobileIconInteractorTest : SysuiTestCase() {
@Test @Test
fun cdma_level_default_unknown() = fun cdma_level_default_unknown() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
mobileSubscriptionRepository.setMobileSubscriptionModel( connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel(isGsm = false), MobileSubscriptionModel(isGsm = false),
SUB_1_ID
) )
var latest: Int? = null var latest: Int? = null
@@ -97,13 +111,12 @@ class MobileIconInteractorTest : SysuiTestCase() {
@Test @Test
fun cdma_usesCdmaLevel() = fun cdma_usesCdmaLevel() =
runBlocking(IMMEDIATE) { runBlocking(IMMEDIATE) {
mobileSubscriptionRepository.setMobileSubscriptionModel( connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel( MobileSubscriptionModel(
isGsm = false, isGsm = false,
primaryLevel = GSM_LEVEL, primaryLevel = GSM_LEVEL,
cdmaLevel = CDMA_LEVEL cdmaLevel = CDMA_LEVEL
), ),
SUB_1_ID
) )
var latest: Int? = null var latest: Int? = null
@@ -114,6 +127,75 @@ class MobileIconInteractorTest : SysuiTestCase() {
job.cancel() 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 { companion object {
private val IMMEDIATE = Dispatchers.Main.immediate private val IMMEDIATE = Dispatchers.Main.immediate
@@ -123,9 +205,5 @@ class MobileIconInteractorTest : SysuiTestCase() {
private const val SUB_1_ID = 1 private const val SUB_1_ID = 1
private val SUB_1 = private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } 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 android.telephony.SubscriptionInfo
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase 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.data.repository.FakeUserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.CarrierConfigTracker
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -39,7 +41,9 @@ import org.mockito.MockitoAnnotations
class MobileIconsInteractorTest : SysuiTestCase() { class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor private lateinit var underTest: MobileIconsInteractor
private val userSetupRepository = FakeUserSetupRepository() 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 @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -47,10 +51,12 @@ class MobileIconsInteractorTest : SysuiTestCase() {
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
underTest = underTest =
MobileIconsInteractor( MobileIconsInteractorImpl(
subscriptionsRepository, subscriptionsRepository,
carrierConfigTracker, carrierConfigTracker,
mobileMappingsProxy,
userSetupRepository, 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"
}
}