Merge "Set up demo mode repository for mobile icons" into tm-qpr-dev

This commit is contained in:
Evan Laird
2022-12-07 13:20:23 +00:00
committed by Android (Google) Code Review
14 changed files with 1902 additions and 453 deletions

View File

@@ -25,7 +25,7 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.Airplane
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 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.MobileRepositorySwitcher
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.MobileIconsInteractor
@@ -63,7 +63,7 @@ abstract class StatusBarPipelineModule {
@Binds @Binds
abstract fun mobileConnectionsRepository( abstract fun mobileConnectionsRepository(
impl: MobileConnectionsRepositoryImpl impl: MobileRepositorySwitcher
): MobileConnectionsRepository ): MobileConnectionsRepository
@Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository

View File

@@ -16,44 +16,13 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository package com.android.systemui.statusbar.pipeline.mobile.data.repository
import android.content.Context
import android.database.ContentObserver
import android.provider.Settings.Global
import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
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.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.android.systemui.util.settings.GlobalSettings
import java.lang.IllegalStateException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/** /**
* Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
@@ -80,183 +49,3 @@ interface MobileConnectionRepository {
*/ */
val isDefaultDataSubscription: StateFlow<Boolean> val isDefaultDataSubscription: StateFlow<Boolean>
} }
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileConnectionRepositoryImpl(
private val context: Context,
private val subId: Int,
private val telephonyManager: TelephonyManager,
private val globalSettings: GlobalSettings,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
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."
)
}
}
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
var state = MobileSubscriptionModel()
conflatedCallbackFlow {
// TODO (b/240569788): log all of these into the connectivity logger
val callback =
object :
TelephonyCallback(),
TelephonyCallback.ServiceStateListener,
TelephonyCallback.SignalStrengthsListener,
TelephonyCallback.DataConnectionStateListener,
TelephonyCallback.DataActivityListener,
TelephonyCallback.CarrierNetworkListener,
TelephonyCallback.DisplayInfoListener {
override fun onServiceStateChanged(serviceState: ServiceState) {
state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
trySend(state)
}
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
val cdmaLevel =
signalStrength
.getCellSignalStrengths(CellSignalStrengthCdma::class.java)
.let { strengths ->
if (!strengths.isEmpty()) {
strengths[0].level
} else {
CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
}
}
val primaryLevel = signalStrength.level
state =
state.copy(
cdmaLevel = cdmaLevel,
primaryLevel = primaryLevel,
isGsm = signalStrength.isGsm,
)
trySend(state)
}
override fun onDataConnectionStateChanged(
dataState: Int,
networkType: Int
) {
state =
state.copy(dataConnectionState = dataState.toDataConnectionType())
trySend(state)
}
override fun onDataActivity(direction: Int) {
state = state.copy(dataActivityDirection = direction)
trySend(state)
}
override fun onCarrierNetworkChange(active: Boolean) {
state = state.copy(carrierNetworkChangeActive = active)
trySend(state)
}
override fun onDisplayInfoChanged(
telephonyDisplayInfo: TelephonyDisplayInfo
) {
val networkType =
if (
telephonyDisplayInfo.overrideNetworkType ==
OVERRIDE_NETWORK_TYPE_NONE
) {
DefaultNetworkType(telephonyDisplayInfo.networkType)
} else {
OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
}
state = state.copy(resolvedNetworkType = networkType)
trySend(state)
}
}
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.onEach { telephonyCallbackEvent.tryEmit(Unit) }
.logOutputChange(logger, "MobileSubscriptionModel")
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
}
/** Produces whenever the mobile data setting changes for this subId */
private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
globalSettings.registerContentObserver(
globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
/* notifyForDescendants */ true,
observer
)
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
/**
* There are a few cases where we will need to poll [TelephonyManager] so we can update some
* internal state where callbacks aren't provided. Any of those events should be merged into
* this flow, which can be used to trigger the polling.
*/
private val telephonyPollingEvent: Flow<Unit> =
merge(
telephonyCallbackEvent,
localMobileDataSettingChangedEvent,
globalMobileDataSettingChangedEvent,
)
override val dataEnabled: StateFlow<Boolean> =
telephonyPollingEvent
.mapLatest { dataConnectionAllowed() }
.stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
override val isDefaultDataSubscription: StateFlow<Boolean> =
defaultDataSubId
.mapLatest { it == subId }
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
class Factory
@Inject
constructor(
private val context: Context,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
private val globalSettings: GlobalSettings,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
fun build(
subId: Int,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
): MobileConnectionRepository {
return MobileConnectionRepositoryImpl(
context,
subId,
telephonyManager.createForSubscriptionId(subId),
globalSettings,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
bgDispatcher,
logger,
scope,
)
}
}
}

View File

@@ -16,53 +16,14 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository package com.android.systemui.statusbar.pipeline.mobile.data.repository
import android.annotation.SuppressLint
import android.content.Context
import android.content.IntentFilter
import android.database.ContentObserver
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.provider.Settings import android.provider.Settings
import android.provider.Settings.Global.MOBILE_DATA
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
import com.android.internal.telephony.PhoneConstants
import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.MobileMappings.Config 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.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.settings.GlobalSettings
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.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
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 * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
@@ -90,202 +51,3 @@ interface MobileConnectionsRepository {
/** Observe changes to the [Settings.Global.MOBILE_DATA] setting */ /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
val globalMobileDataSettingChangedEvent: Flow<Unit> val globalMobileDataSettingChangedEvent: Flow<Unit>
} }
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class MobileConnectionsRepositoryImpl
@Inject
constructor(
private val connectivityManager: ConnectivityManager,
private val subscriptionManager: SubscriptionManager,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
broadcastDispatcher: BroadcastDispatcher,
private val globalSettings: GlobalSettings,
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(), INVALID_SUBSCRIPTION_ID)
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
MutableSharedFlow(extraBufferCapacity = 1)
override val defaultDataSubId: StateFlow<Int> =
broadcastDispatcher
.broadcastFlow(
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
) { intent, _ ->
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
}
.distinctUntilChanged()
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
SubscriptionManager.getDefaultDataSubscriptionId()
)
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> =
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
.mapLatest { 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 }
}
/**
* In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
* connection repositories also observe the URI for [MOBILE_DATA] + subId.
*/
override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
globalSettings.registerContentObserver(
globalSettings.getUriFor(MOBILE_DATA),
true,
observer
)
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
@SuppressLint("MissingPermission")
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
conflatedCallbackFlow {
val callback =
object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onLost(network: Network) {
// Send a disconnected model when lost. Maybe should create a sealed
// type or null here?
trySend(MobileConnectivityModel())
}
override fun onCapabilitiesChanged(
network: Network,
caps: NetworkCapabilities
) {
trySend(
MobileConnectivityModel(
isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
)
)
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
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,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
)
}
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

@@ -0,0 +1,162 @@
/*
* 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.os.Bundle
import android.telephony.SubscriptionInfo
import androidx.annotation.VisibleForTesting
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
/**
* A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and
* Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], which
* switches based on the latest information from [DemoModeController], and switches every flow in
* the interface to point to the currently-active provider. This allows us to put the demo mode
* interface in its own repository, completely separate from the real version, while still using all
* of the prod implementations for the rest of the pipeline (interactors and onward). Looks
* something like this:
*
* ```
* RealRepository
* │
* ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
* │
* DemoRepository
* ```
*
* NOTE: because the UI layer for mobile icons relies on a nested-repository structure, it is likely
* that we will have to drain the subscription list whenever demo mode changes. Otherwise if a real
* subscription list [1] is replaced with a demo subscription list [1], the view models will not see
* a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo
* implementation.
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileRepositorySwitcher
@Inject
constructor(
@Application scope: CoroutineScope,
val realRepository: MobileConnectionsRepositoryImpl,
val demoMobileConnectionsRepository: DemoMobileConnectionsRepository,
demoModeController: DemoModeController,
) : MobileConnectionsRepository {
val isDemoMode: StateFlow<Boolean> =
conflatedCallbackFlow {
val callback =
object : DemoMode {
override fun dispatchDemoCommand(command: String?, args: Bundle?) {
// Nothing, we just care about on/off
}
override fun onDemoModeStarted() {
demoMobileConnectionsRepository.startProcessingCommands()
trySend(true)
}
override fun onDemoModeFinished() {
demoMobileConnectionsRepository.stopProcessingCommands()
trySend(false)
}
}
demoModeController.addCallback(callback)
awaitClose { demoModeController.removeCallback(callback) }
}
.stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
// Convenient definition flow for the currently active repo (based on demo mode or not)
@VisibleForTesting
internal val activeRepo: StateFlow<MobileConnectionsRepository> =
isDemoMode
.mapLatest { demoMode ->
if (demoMode) {
demoMobileConnectionsRepository
} else {
realRepository
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), realRepository)
override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
activeRepo
.flatMapLatest { it.subscriptionsFlow }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
realRepository.subscriptionsFlow.value
)
override val activeMobileDataSubscriptionId: StateFlow<Int> =
activeRepo
.flatMapLatest { it.activeMobileDataSubscriptionId }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
realRepository.activeMobileDataSubscriptionId.value
)
override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
activeRepo
.flatMapLatest { it.defaultDataSubRatConfig }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
realRepository.defaultDataSubRatConfig.value
)
override val defaultDataSubId: StateFlow<Int> =
activeRepo
.flatMapLatest { it.defaultDataSubId }
.stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
activeRepo
.flatMapLatest { it.defaultMobileNetworkConnectivity }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
realRepository.defaultMobileNetworkConnectivity.value
)
override val globalMobileDataSettingChangedEvent: Flow<Unit> =
activeRepo.flatMapLatest { it.globalMobileDataSettingChangedEvent }
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
if (isDemoMode.value) {
return demoMobileConnectionsRepository.getRepoForSubId(subId)
}
return realRepository.getRepoForSubId(subId)
}
}

View File

@@ -0,0 +1,277 @@
/*
* 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.demo
import android.content.Context
import android.telephony.Annotation
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED
import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_NR
import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import android.util.Log
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
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.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** This repository vends out data based on demo mode commands */
@OptIn(ExperimentalCoroutinesApi::class)
class DemoMobileConnectionsRepository
@Inject
constructor(
private val dataSource: DemoModeMobileConnectionDataSource,
@Application private val scope: CoroutineScope,
context: Context,
) : MobileConnectionsRepository {
private var demoCommandJob: Job? = null
private val connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionInfo>()
val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
private val _subscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
override val subscriptionsFlow =
_subscriptions
.onEach { infos -> dropUnusedReposFromCache(infos) }
.stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value)
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 }
connectionRepoCache.keys.forEach {
if (!currentValidSubscriptionIds.contains(it)) {
connectionRepoCache.remove(it)
}
}
}
private fun maybeCreateSubscription(subId: Int) {
if (!subscriptionInfoCache.containsKey(subId)) {
createSubscriptionForSubId(subId, subId).also { subscriptionInfoCache[subId] = it }
_subscriptions.value = subscriptionInfoCache.values.toList()
}
}
/** Mimics the old NetworkControllerImpl for now */
private fun createSubscriptionForSubId(subId: Int, slotIndex: Int): SubscriptionInfo {
return SubscriptionInfo(
subId,
"",
slotIndex,
"",
"",
0,
0,
"",
0,
null,
null,
null,
"",
false,
null,
null,
)
}
// TODO(b/261029387): add a command for this value
override val activeMobileDataSubscriptionId =
subscriptionsFlow
.mapLatest { infos ->
// For now, active is just the first in the list
infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
}
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
subscriptionsFlow.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
)
/** Demo mode doesn't currently support modifications to the mobile mappings */
override val defaultDataSubRatConfig =
MutableStateFlow(MobileMappings.Config.readConfig(context))
// TODO(b/261029387): add a command for this value
override val defaultDataSubId =
activeMobileDataSubscriptionId.stateIn(
scope,
SharingStarted.WhileSubscribed(),
INVALID_SUBSCRIPTION_ID
)
// TODO(b/261029387): not yet supported
override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
return connectionRepoCache[subId]
?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it }
}
override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
fun startProcessingCommands() {
demoCommandJob =
scope.launch {
dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
}
}
fun stopProcessingCommands() {
demoCommandJob?.cancel()
_subscriptions.value = listOf()
connectionRepoCache.clear()
subscriptionInfoCache.clear()
}
private fun processEvent(event: FakeNetworkEventModel) {
when (event) {
is Mobile -> {
processEnabledMobileState(event)
}
is MobileDisabled -> {
processDisabledMobileState(event)
}
}
}
private fun processEnabledMobileState(state: Mobile) {
// get or create the connection repo, and set its values
val subId = state.subId ?: DEFAULT_SUB_ID
maybeCreateSubscription(subId)
val connection = getRepoForSubId(subId)
// This is always true here, because we split out disabled states at the data-source level
connection.dataEnabled.value = true
connection.isDefaultDataSubscription.value = state.dataType != null
connection.subscriptionModelFlow.value = state.toMobileSubscriptionModel()
}
private fun processDisabledMobileState(state: MobileDisabled) {
if (_subscriptions.value.isEmpty()) {
// Nothing to do here
return
}
val subId =
state.subId
?: run {
// For sake of usability, we can allow for no subId arg if there is only one
// subscription
if (_subscriptions.value.size > 1) {
Log.d(
TAG,
"processDisabledMobileState: Unable to infer subscription to " +
"disable. Specify subId using '-e slot <subId>'" +
"Known subIds: [${subIdsString()}]"
)
return
}
// Use the only existing subscription as our arg, since there is only one
_subscriptions.value[0].subscriptionId
}
removeSubscription(subId)
}
private fun removeSubscription(subId: Int) {
val currentSubscriptions = _subscriptions.value
subscriptionInfoCache.remove(subId)
_subscriptions.value = currentSubscriptions.filter { it.subscriptionId != subId }
}
private fun subIdsString(): String =
_subscriptions.value.joinToString(",") { it.subscriptionId.toString() }
companion object {
private const val TAG = "DemoMobileConnectionsRepo"
private const val DEFAULT_SUB_ID = 1
}
}
private fun Mobile.toMobileSubscriptionModel(): MobileSubscriptionModel {
return MobileSubscriptionModel(
isEmergencyOnly = false, // TODO(b/261029387): not yet supported
isGsm = false, // TODO(b/261029387): not yet supported
cdmaLevel = level ?: 0,
primaryLevel = level ?: 0,
dataConnectionState = DataConnectionState.Connected, // TODO(b/261029387): not yet supported
dataActivityDirection = activity,
carrierNetworkChangeActive = carrierNetworkChange,
// TODO(b/261185097): once mobile mappings can be mocked at this layer, we can build our
// own demo map
resolvedNetworkType = dataType.toResolvedNetworkType()
)
}
@Annotation.NetworkType
private fun SignalIcon.MobileIconGroup?.toNetworkType(): Int =
when (this) {
TelephonyIcons.THREE_G -> NETWORK_TYPE_GSM
TelephonyIcons.LTE -> NETWORK_TYPE_LTE
TelephonyIcons.FOUR_G -> NETWORK_TYPE_UMTS
TelephonyIcons.NR_5G -> NETWORK_TYPE_NR
TelephonyIcons.NR_5G_PLUS -> OVERRIDE_NETWORK_TYPE_NR_ADVANCED
else -> NETWORK_TYPE_UNKNOWN
}
private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType =
when (this) {
TelephonyIcons.NR_5G_PLUS -> OverrideNetworkType(toNetworkType())
else -> DefaultNetworkType(toNetworkType())
}
class DemoMobileConnectionRepository(val subId: Int) : MobileConnectionRepository {
override val subscriptionModelFlow = MutableStateFlow(MobileSubscriptionModel())
override val dataEnabled = MutableStateFlow(true)
override val isDefaultDataSubscription = MutableStateFlow(true)
}

View File

@@ -0,0 +1,141 @@
/*
* 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.demo
import android.os.Bundle
import android.telephony.Annotation.DataActivityType
import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
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.demomode.DemoMode
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
/**
* Data source that can map from demo mode commands to inputs into the
* [DemoMobileConnectionsRepository]'s flows
*/
@SysUISingleton
class DemoModeMobileConnectionDataSource
@Inject
constructor(
demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) {
private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
val callback =
object : DemoMode {
override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
override fun dispatchDemoCommand(command: String, args: Bundle) {
trySend(args)
}
override fun onDemoModeFinished() {
// Handled elsewhere
}
override fun onDemoModeStarted() {
// Handled elsewhere
}
}
demoModeController.addCallback(callback)
awaitClose { demoModeController.removeCallback(callback) }
}
// If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
// commands work and it's a little silly
private val _mobileCommands = demoCommandStream.map { args -> args.toMobileEvent() }
val mobileEvents = _mobileCommands.shareIn(scope, SharingStarted.WhileSubscribed())
private fun Bundle.toMobileEvent(): FakeNetworkEventModel? {
val mobile = getString("mobile") ?: return null
return if (mobile == "show") {
activeMobileEvent()
} else {
MobileDisabled(subId = getString("slot")?.toInt())
}
}
/** Parse a valid mobile command string into a network event */
private fun Bundle.activeMobileEvent(): Mobile {
// There are many key/value pairs supported by mobile demo mode. Bear with me here
val level = getString("level")?.toInt()
val dataType = getString("datatype")?.toDataType()
val slot = getString("slot")?.toInt()
val carrierId = getString("carrierid")?.toInt()
val inflateStrength = getString("inflate")?.toBoolean()
val activity = getString("activity")?.toActivity()
val carrierNetworkChange = getString("carriernetworkchange") == "show"
return Mobile(
level = level,
dataType = dataType,
subId = slot,
carrierId = carrierId,
inflateStrength = inflateStrength,
activity = activity,
carrierNetworkChange = carrierNetworkChange,
)
}
}
private fun String.toDataType(): MobileIconGroup =
when (this) {
"1x" -> TelephonyIcons.ONE_X
"3g" -> TelephonyIcons.THREE_G
"4g" -> TelephonyIcons.FOUR_G
"4g+" -> TelephonyIcons.FOUR_G_PLUS
"5g" -> TelephonyIcons.NR_5G
"5ge" -> TelephonyIcons.LTE_CA_5G_E
"5g+" -> TelephonyIcons.NR_5G_PLUS
"e" -> TelephonyIcons.E
"g" -> TelephonyIcons.G
"h" -> TelephonyIcons.H
"h+" -> TelephonyIcons.H_PLUS
"lte" -> TelephonyIcons.LTE
"lte+" -> TelephonyIcons.LTE_PLUS
"dis" -> TelephonyIcons.DATA_DISABLED
"not" -> TelephonyIcons.NOT_DEFAULT_DATA
else -> TelephonyIcons.UNKNOWN
}
@DataActivityType
private fun String.toActivity(): Int =
when (this) {
"inout" -> DATA_ACTIVITY_INOUT
"in" -> DATA_ACTIVITY_IN
"out" -> DATA_ACTIVITY_OUT
else -> DATA_ACTIVITY_NONE
}

View File

@@ -0,0 +1,43 @@
/*
* 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.demo.model
import android.telephony.Annotation.DataActivityType
import com.android.settingslib.SignalIcon
/**
* Model for the demo commands, ported from [NetworkControllerImpl]
*
* Nullable fields represent optional command line arguments
*/
sealed interface FakeNetworkEventModel {
data class Mobile(
val level: Int?,
val dataType: SignalIcon.MobileIconGroup?,
// Null means the default (chosen by the repository)
val subId: Int?,
val carrierId: Int?,
val inflateStrength: Boolean?,
@DataActivityType val activity: Int?,
val carrierNetworkChange: Boolean,
) : FakeNetworkEventModel
data class MobileDisabled(
// Null means the default (chosen by the repository)
val subId: Int?
) : FakeNetworkEventModel
}

View File

@@ -0,0 +1,235 @@
/*
* 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.prod
import android.content.Context
import android.database.ContentObserver
import android.provider.Settings.Global
import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
import android.telephony.SignalStrength
import android.telephony.TelephonyCallback
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.android.systemui.util.settings.GlobalSettings
import java.lang.IllegalStateException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileConnectionRepositoryImpl(
private val context: Context,
private val subId: Int,
private val telephonyManager: TelephonyManager,
private val globalSettings: GlobalSettings,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
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."
)
}
}
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
var state = MobileSubscriptionModel()
conflatedCallbackFlow {
// TODO (b/240569788): log all of these into the connectivity logger
val callback =
object :
TelephonyCallback(),
TelephonyCallback.ServiceStateListener,
TelephonyCallback.SignalStrengthsListener,
TelephonyCallback.DataConnectionStateListener,
TelephonyCallback.DataActivityListener,
TelephonyCallback.CarrierNetworkListener,
TelephonyCallback.DisplayInfoListener {
override fun onServiceStateChanged(serviceState: ServiceState) {
state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
trySend(state)
}
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
val cdmaLevel =
signalStrength
.getCellSignalStrengths(CellSignalStrengthCdma::class.java)
.let { strengths ->
if (!strengths.isEmpty()) {
strengths[0].level
} else {
CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
}
}
val primaryLevel = signalStrength.level
state =
state.copy(
cdmaLevel = cdmaLevel,
primaryLevel = primaryLevel,
isGsm = signalStrength.isGsm,
)
trySend(state)
}
override fun onDataConnectionStateChanged(
dataState: Int,
networkType: Int
) {
state =
state.copy(dataConnectionState = dataState.toDataConnectionType())
trySend(state)
}
override fun onDataActivity(direction: Int) {
state = state.copy(dataActivityDirection = direction)
trySend(state)
}
override fun onCarrierNetworkChange(active: Boolean) {
state = state.copy(carrierNetworkChangeActive = active)
trySend(state)
}
override fun onDisplayInfoChanged(
telephonyDisplayInfo: TelephonyDisplayInfo
) {
val networkType =
if (
telephonyDisplayInfo.overrideNetworkType ==
OVERRIDE_NETWORK_TYPE_NONE
) {
DefaultNetworkType(telephonyDisplayInfo.networkType)
} else {
OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
}
state = state.copy(resolvedNetworkType = networkType)
trySend(state)
}
}
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.onEach { telephonyCallbackEvent.tryEmit(Unit) }
.logOutputChange(logger, "MobileSubscriptionModel")
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
}
/** Produces whenever the mobile data setting changes for this subId */
private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
globalSettings.registerContentObserver(
globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
/* notifyForDescendants */ true,
observer
)
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
/**
* There are a few cases where we will need to poll [TelephonyManager] so we can update some
* internal state where callbacks aren't provided. Any of those events should be merged into
* this flow, which can be used to trigger the polling.
*/
private val telephonyPollingEvent: Flow<Unit> =
merge(
telephonyCallbackEvent,
localMobileDataSettingChangedEvent,
globalMobileDataSettingChangedEvent,
)
override val dataEnabled: StateFlow<Boolean> =
telephonyPollingEvent
.mapLatest { dataConnectionAllowed() }
.stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
override val isDefaultDataSubscription: StateFlow<Boolean> =
defaultDataSubId
.mapLatest { it == subId }
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
class Factory
@Inject
constructor(
private val context: Context,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
private val globalSettings: GlobalSettings,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
fun build(
subId: Int,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
): MobileConnectionRepository {
return MobileConnectionRepositoryImpl(
context,
subId,
telephonyManager.createForSubscriptionId(subId),
globalSettings,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
bgDispatcher,
logger,
scope,
)
}
}
}

View File

@@ -0,0 +1,265 @@
/*
* 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.prod
import android.annotation.SuppressLint
import android.content.Context
import android.content.IntentFilter
import android.database.ContentObserver
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.provider.Settings.Global.MOBILE_DATA
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
import com.android.internal.telephony.PhoneConstants
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.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.settings.GlobalSettings
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.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class MobileConnectionsRepositoryImpl
@Inject
constructor(
private val connectivityManager: ConnectivityManager,
private val subscriptionManager: SubscriptionManager,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
broadcastDispatcher: BroadcastDispatcher,
private val globalSettings: GlobalSettings,
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(), INVALID_SUBSCRIPTION_ID)
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
MutableSharedFlow(extraBufferCapacity = 1)
override val defaultDataSubId: StateFlow<Int> =
broadcastDispatcher
.broadcastFlow(
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
) { intent, _ ->
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
}
.distinctUntilChanged()
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
SubscriptionManager.getDefaultDataSubscriptionId()
)
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> =
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
.mapLatest { 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 }
}
/**
* In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
* connection repositories also observe the URI for [MOBILE_DATA] + subId.
*/
override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
globalSettings.registerContentObserver(
globalSettings.getUriFor(MOBILE_DATA),
true,
observer
)
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
@SuppressLint("MissingPermission")
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
conflatedCallbackFlow {
val callback =
object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onLost(network: Network) {
// Send a disconnected model when lost. Maybe should create a sealed
// type or null here?
trySend(MobileConnectivityModel())
}
override fun onCapabilitiesChanged(
network: Network,
caps: NetworkCapabilities
) {
trySend(
MobileConnectivityModel(
isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
)
)
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
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,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
)
}
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

@@ -0,0 +1,219 @@
/*
* 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.net.ConnectivityManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
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.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
/**
* The switcher acts as a dispatcher to either the `prod` or `demo` versions of the repository
* interface it's switching on. These tests just need to verify that the entire interface properly
* switches over when the value of `demoMode` changes
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class MobileRepositorySwitcherTest : SysuiTestCase() {
private lateinit var underTest: MobileRepositorySwitcher
private lateinit var realRepo: MobileConnectionsRepositoryImpl
private lateinit var demoRepo: DemoMobileConnectionsRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var demoModeController: DemoModeController
private val globalSettings = FakeSettings()
private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
private val scope = CoroutineScope(IMMEDIATE)
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
// Never start in demo mode
whenever(demoModeController.isInDemoMode).thenReturn(false)
mockDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
}
realRepo =
MobileConnectionsRepositoryImpl(
connectivityManager,
subscriptionManager,
telephonyManager,
logger,
fakeBroadcastDispatcher,
globalSettings,
context,
IMMEDIATE,
scope,
mock(),
)
demoRepo =
DemoMobileConnectionsRepository(
dataSource = mockDataSource,
scope = scope,
context = context,
)
underTest =
MobileRepositorySwitcher(
scope = scope,
realRepository = realRepo,
demoMobileConnectionsRepository = demoRepo,
demoModeController = demoModeController,
)
}
@After
fun tearDown() {
scope.cancel()
}
@Test
fun `active repo matches demo mode setting`() =
runBlocking(IMMEDIATE) {
whenever(demoModeController.isInDemoMode).thenReturn(false)
var latest: MobileConnectionsRepository? = null
val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
assertThat(latest).isEqualTo(realRepo)
startDemoMode()
assertThat(latest).isEqualTo(demoRepo)
finishDemoMode()
assertThat(latest).isEqualTo(realRepo)
job.cancel()
}
@Test
fun `subscription list updates when demo mode changes`() =
runBlocking(IMMEDIATE) {
whenever(demoModeController.isInDemoMode).thenReturn(false)
whenever(subscriptionManager.completeActiveSubscriptionInfoList)
.thenReturn(listOf(SUB_1, SUB_2))
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
// The real subscriptions has 2 subs
whenever(subscriptionManager.completeActiveSubscriptionInfoList)
.thenReturn(listOf(SUB_1, SUB_2))
getSubscriptionCallback().onSubscriptionsChanged()
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
// Demo mode turns on, and we should see only the demo subscriptions
startDemoMode()
fakeNetworkEventsFlow.value = validMobileEvent(subId = 3)
// Demo mobile connections repository makes arbitrarily-formed subscription info
// objects, so just validate the data we care about
assertThat(latest).hasSize(1)
assertThat(latest!![0].subscriptionId).isEqualTo(3)
finishDemoMode()
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
job.cancel()
}
private fun startDemoMode() {
whenever(demoModeController.isInDemoMode).thenReturn(true)
getDemoModeCallback().onDemoModeStarted()
}
private fun finishDemoMode() {
whenever(demoModeController.isInDemoMode).thenReturn(false)
getDemoModeCallback().onDemoModeFinished()
}
private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
val callbackCaptor =
kotlinArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
verify(subscriptionManager)
.addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
return callbackCaptor.value
}
private fun getDemoModeCallback(): DemoMode {
val captor = kotlinArgumentCaptor<DemoMode>()
verify(demoModeController).addCallback(captor.capture())
return captor.value
}
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

@@ -0,0 +1,249 @@
/*
* 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.demo
import android.telephony.Annotation
import android.telephony.TelephonyManager
import androidx.test.filters.SmallTest
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
/**
* Parameterized test for all of the common values of [FakeNetworkEventModel]. This test simply
* verifies that passing the given model to [DemoMobileConnectionsRepository] results in the correct
* flows emitting from the given connection.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(Parameterized::class)
internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) :
SysuiTestCase() {
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
private lateinit var connectionsRepo: DemoMobileConnectionsRepository
private lateinit var underTest: DemoMobileConnectionRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
@Before
fun setUp() {
// The data source only provides one API, so we can mock it with a flow here for convenience
mockDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
}
connectionsRepo =
DemoMobileConnectionsRepository(
dataSource = mockDataSource,
scope = testScope.backgroundScope,
context = context,
)
connectionsRepo.startProcessingCommands()
}
@After
fun tearDown() {
testScope.cancel()
}
@Test
fun demoNetworkData() =
testScope.runTest {
val networkModel =
FakeNetworkEventModel.Mobile(
level = testCase.level,
dataType = testCase.dataType,
subId = testCase.subId,
carrierId = testCase.carrierId,
inflateStrength = testCase.inflateStrength,
activity = testCase.activity,
carrierNetworkChange = testCase.carrierNetworkChange,
)
fakeNetworkEventFlow.value = networkModel
underTest = connectionsRepo.getRepoForSubId(subId)
assertConnection(underTest, networkModel)
}
private fun assertConnection(
conn: DemoMobileConnectionRepository,
model: FakeNetworkEventModel
) {
when (model) {
is FakeNetworkEventModel.Mobile -> {
val subscriptionModel: MobileSubscriptionModel = conn.subscriptionModelFlow.value
assertThat(conn.subId).isEqualTo(model.subId)
assertThat(subscriptionModel.cdmaLevel).isEqualTo(model.level)
assertThat(subscriptionModel.primaryLevel).isEqualTo(model.level)
assertThat(subscriptionModel.dataActivityDirection).isEqualTo(model.activity)
assertThat(subscriptionModel.carrierNetworkChangeActive)
.isEqualTo(model.carrierNetworkChange)
// TODO(b/261029387): check these once we start handling them
assertThat(subscriptionModel.isEmergencyOnly).isFalse()
assertThat(subscriptionModel.isGsm).isFalse()
assertThat(subscriptionModel.dataConnectionState)
.isEqualTo(DataConnectionState.Connected)
}
// MobileDisabled isn't combinatorial in nature, and is tested in
// DemoMobileConnectionsRepositoryTest.kt
else -> {}
}
}
/** Matches [FakeNetworkEventModel] */
internal data class TestCase(
val level: Int,
val dataType: SignalIcon.MobileIconGroup,
val subId: Int,
val carrierId: Int,
val inflateStrength: Boolean,
@Annotation.DataActivityType val activity: Int,
val carrierNetworkChange: Boolean,
) {
override fun toString(): String {
return "INPUT(level=$level, " +
"dataType=${dataType.name}, " +
"subId=$subId, " +
"carrierId=$carrierId, " +
"inflateStrength=$inflateStrength, " +
"activity=$activity, " +
"carrierNetworkChange=$carrierNetworkChange)"
}
// Convenience for iterating test data and creating new cases
fun modifiedBy(
level: Int? = null,
dataType: SignalIcon.MobileIconGroup? = null,
subId: Int? = null,
carrierId: Int? = null,
inflateStrength: Boolean? = null,
@Annotation.DataActivityType activity: Int? = null,
carrierNetworkChange: Boolean? = null,
): TestCase =
TestCase(
level = level ?: this.level,
dataType = dataType ?: this.dataType,
subId = subId ?: this.subId,
carrierId = carrierId ?: this.carrierId,
inflateStrength = inflateStrength ?: this.inflateStrength,
activity = activity ?: this.activity,
carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange
)
}
companion object {
private val subId = 1
private val booleanList = listOf(true, false)
private val levels = listOf(0, 1, 2, 3)
private val dataTypes =
listOf(
TelephonyIcons.THREE_G,
TelephonyIcons.LTE,
TelephonyIcons.FOUR_G,
TelephonyIcons.NR_5G,
TelephonyIcons.NR_5G_PLUS,
)
private val carrierIds = listOf(1, 10, 100)
private val inflateStrength = booleanList
private val activity =
listOf(
TelephonyManager.DATA_ACTIVITY_NONE,
TelephonyManager.DATA_ACTIVITY_IN,
TelephonyManager.DATA_ACTIVITY_OUT,
TelephonyManager.DATA_ACTIVITY_INOUT
)
private val carrierNetworkChange = booleanList
@Parameters(name = "{0}") @JvmStatic fun data() = testData()
/**
* Generate some test data. For the sake of convenience, we'll parameterize only non-null
* network event data. So given the lists of test data:
* ```
* list1 = [1, 2, 3]
* list2 = [false, true]
* list3 = [a, b, c]
* ```
* We'll generate test cases for:
*
* Test (1, false, a) Test (2, false, a) Test (3, false, a) Test (1, true, a) Test (1,
* false, b) Test (1, false, c)
*
* NOTE: this is not a combinatorial product of all of the possible sets of parameters.
* Since this test is built to exercise demo mode, the general approach is to define a
* fully-formed "base case", and from there to make sure to use every valid parameter once,
* by defining the rest of the test cases against the base case. Specific use-cases can be
* added to the non-parameterized test, or manually below the generated test cases.
*/
private fun testData(): List<TestCase> {
val testSet = mutableSetOf<TestCase>()
val baseCase =
TestCase(
levels.first(),
dataTypes.first(),
subId,
carrierIds.first(),
inflateStrength.first(),
activity.first(),
carrierNetworkChange.first()
)
val tail =
sequenceOf(
levels.map { baseCase.modifiedBy(level = it) },
dataTypes.map { baseCase.modifiedBy(dataType = it) },
carrierIds.map { baseCase.modifiedBy(carrierId = it) },
inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
activity.map { baseCase.modifiedBy(activity = it) },
carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
)
.flatten()
testSet.add(baseCase)
tail.toCollection(testSet)
return testSet.toList()
}
}
}

View File

@@ -0,0 +1,306 @@
/*
* 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.demo
import android.telephony.SubscriptionInfo
import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
import androidx.test.filters.SmallTest
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons.THREE_G
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class DemoMobileConnectionsRepositoryTest : SysuiTestCase() {
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
private lateinit var underTest: DemoMobileConnectionsRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
@Before
fun setUp() {
// The data source only provides one API, so we can mock it with a flow here for convenience
mockDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
}
underTest =
DemoMobileConnectionsRepository(
dataSource = mockDataSource,
scope = testScope.backgroundScope,
context = context,
)
underTest.startProcessingCommands()
}
@Test
fun `network event - create new subscription`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
assertThat(latest).isEmpty()
fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
assertThat(latest).hasSize(1)
assertThat(latest!![0].subscriptionId).isEqualTo(1)
job.cancel()
}
@Test
fun `network event - reuses subscription when same Id`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
assertThat(latest).isEmpty()
fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
assertThat(latest).hasSize(1)
assertThat(latest!![0].subscriptionId).isEqualTo(1)
// Second network event comes in with the same subId, does not create a new subscription
fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 2)
assertThat(latest).hasSize(1)
assertThat(latest!![0].subscriptionId).isEqualTo(1)
job.cancel()
}
@Test
fun `multiple subscriptions`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
assertThat(latest).hasSize(2)
job.cancel()
}
@Test
fun `mobile disabled event - disables connection - subId specified - single conn`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
fakeNetworkEventFlow.value = MobileDisabled(subId = 1)
assertThat(latest).hasSize(0)
job.cancel()
}
@Test
fun `mobile disabled event - disables connection - subId not specified - single conn`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
fakeNetworkEventFlow.value = MobileDisabled(subId = null)
assertThat(latest).hasSize(0)
job.cancel()
}
@Test
fun `mobile disabled event - disables connection - subId specified - multiple conn`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
fakeNetworkEventFlow.value = MobileDisabled(subId = 2)
assertThat(latest).hasSize(1)
job.cancel()
}
@Test
fun `mobile disabled event - subId not specified - multiple conn - ignores command`() =
testScope.runTest {
var latest: List<SubscriptionInfo>? = null
val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
fakeNetworkEventFlow.value = MobileDisabled(subId = null)
assertThat(latest).hasSize(2)
job.cancel()
}
@Test
fun `demo connection - single subscription`() =
testScope.runTest {
var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1)
var connections: List<DemoMobileConnectionRepository>? = null
val job =
underTest.subscriptionsFlow
.onEach { infos ->
connections =
infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
}
.launchIn(this)
fakeNetworkEventFlow.value = currentEvent
assertThat(connections).hasSize(1)
val connection1 = connections!![0]
assertConnection(connection1, currentEvent)
// Exercise the whole api
currentEvent = validMobileEvent(subId = 1, level = 2)
fakeNetworkEventFlow.value = currentEvent
assertConnection(connection1, currentEvent)
job.cancel()
}
@Test
fun `demo connection - two connections - update second - no affect on first`() =
testScope.runTest {
var currentEvent1 = validMobileEvent(subId = 1)
var connection1: DemoMobileConnectionRepository? = null
var currentEvent2 = validMobileEvent(subId = 2)
var connection2: DemoMobileConnectionRepository? = null
var connections: List<DemoMobileConnectionRepository>? = null
val job =
underTest.subscriptionsFlow
.onEach { infos ->
connections =
infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
}
.launchIn(this)
fakeNetworkEventFlow.value = currentEvent1
fakeNetworkEventFlow.value = currentEvent2
assertThat(connections).hasSize(2)
connections!!.forEach {
if (it.subId == 1) {
connection1 = it
} else if (it.subId == 2) {
connection2 = it
} else {
Assert.fail("Unexpected subscription")
}
}
assertConnection(connection1!!, currentEvent1)
assertConnection(connection2!!, currentEvent2)
// WHEN the event changes for connection 2, it updates, and connection 1 stays the same
currentEvent2 = validMobileEvent(subId = 2, activity = DATA_ACTIVITY_INOUT)
fakeNetworkEventFlow.value = currentEvent2
assertConnection(connection1!!, currentEvent1)
assertConnection(connection2!!, currentEvent2)
// and vice versa
currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
fakeNetworkEventFlow.value = currentEvent1
assertConnection(connection1!!, currentEvent1)
assertConnection(connection2!!, currentEvent2)
job.cancel()
}
private fun assertConnection(
conn: DemoMobileConnectionRepository,
model: FakeNetworkEventModel
) {
when (model) {
is FakeNetworkEventModel.Mobile -> {
val subscriptionModel: MobileSubscriptionModel = conn.subscriptionModelFlow.value
assertThat(conn.subId).isEqualTo(model.subId)
assertThat(subscriptionModel.cdmaLevel).isEqualTo(model.level)
assertThat(subscriptionModel.primaryLevel).isEqualTo(model.level)
assertThat(subscriptionModel.dataActivityDirection).isEqualTo(model.activity)
assertThat(subscriptionModel.carrierNetworkChangeActive)
.isEqualTo(model.carrierNetworkChange)
// TODO(b/261029387) check these once we start handling them
assertThat(subscriptionModel.isEmergencyOnly).isFalse()
assertThat(subscriptionModel.isGsm).isFalse()
assertThat(subscriptionModel.dataConnectionState)
.isEqualTo(DataConnectionState.Connected)
}
else -> {}
}
}
}
/** Convenience to create a valid fake network event with minimal params */
fun validMobileEvent(
level: Int? = 1,
dataType: SignalIcon.MobileIconGroup? = THREE_G,
subId: Int? = 1,
carrierId: Int? = UNKNOWN_CARRIER_ID,
inflateStrength: Boolean? = false,
activity: Int? = null,
carrierNetworkChange: Boolean = false,
): FakeNetworkEventModel =
FakeNetworkEventModel.Mobile(
level = level,
dataType = dataType,
subId = subId,
carrierId = carrierId,
inflateStrength = inflateStrength,
activity = activity,
carrierNetworkChange = carrierNetworkChange,
)

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.statusbar.pipeline.mobile.data.repository package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.os.UserHandle import android.os.UserHandle
import android.provider.Settings import android.provider.Settings
@@ -40,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionS
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType 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.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger 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

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.statusbar.pipeline.mobile.data.repository package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager