Merge "Set up demo mode repository for mobile icons" into tm-qpr-dev am: a2c6879a83 am: f7e551d8e5
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20612834 Change-Id: I46a27e58de2e1add77201c996d491f36d0451f3f Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -25,7 +25,7 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.Airplane
|
||||
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
|
||||
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
|
||||
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
|
||||
@@ -63,7 +63,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