Set up demo mode repository for mobile icons
This CL is the sketch of how we can achieve demo mode using the modern
architecture that makes up the new data pipelines. The basic idea is to
put a `RepositorySwitcher` in between the real repository impl and the
demo mode repository.
RealRepository
│
├──►RepositorySwitcher──►RealInteractor──►RealViewModel
│
DemoRepository
In this case, we have a `IRepository` interface which both the demo and
fake repositories implement, and the switcher decides which
implementation to connect to the (real) downstream classes based on
whether or not demo mode is active.
Note that this CL does not hook up the demo mode repository properly,
since the old data pipeline uses a demo icon group layered on top of the
real one to achieve fake icons. That makes things kind of a mess and so
will be implemented in its own CL
Test: DemoMobileConnectionParameterizedTest
Test: DemoMobileConnectionsRepositoryTest
Test: MobileRepositorySwitcherTest
Bug: 249790009
Change-Id: I9ad6bab6dd0ab6eadf43232e3bea0f4fa3116a17
This commit is contained in:
@@ -23,7 +23,7 @@ import com.android.systemui.log.table.TableLogBufferFactory
|
||||
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
|
||||
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.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.UserSetupRepositoryImpl
|
||||
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
|
||||
@@ -53,7 +53,7 @@ abstract class StatusBarPipelineModule {
|
||||
|
||||
@Binds
|
||||
abstract fun mobileConnectionsRepository(
|
||||
impl: MobileConnectionsRepositoryImpl
|
||||
impl: MobileRepositorySwitcher
|
||||
): MobileConnectionsRepository
|
||||
|
||||
@Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
|
||||
|
||||
@@ -16,44 +16,13 @@
|
||||
|
||||
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.SubscriptionManager
|
||||
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.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
|
||||
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,53 +16,14 @@
|
||||
|
||||
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.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.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
|
||||
|
||||
/**
|
||||
* 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 */
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.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.MobileSubscriptionModel
|
||||
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.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.net.ConnectivityManager
|
||||
Reference in New Issue
Block a user