Merge "Set up demo mode repository for mobile icons" into tm-qpr-dev am: a2c6879a83
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20612834 Change-Id: I229f9292a2ef76bf73ae1f7b8fefc7e7c3175001 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.AirplaneModeViewModel
|
||||||
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
|
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
|
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
|
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
|
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
|
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
|
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
|
||||||
@@ -63,7 +63,7 @@ abstract class StatusBarPipelineModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun mobileConnectionsRepository(
|
abstract fun mobileConnectionsRepository(
|
||||||
impl: MobileConnectionsRepositoryImpl
|
impl: MobileRepositorySwitcher
|
||||||
): MobileConnectionsRepository
|
): MobileConnectionsRepository
|
||||||
|
|
||||||
@Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
|
@Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
|
||||||
|
|||||||
@@ -16,44 +16,13 @@
|
|||||||
|
|
||||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.ContentObserver
|
|
||||||
import android.provider.Settings.Global
|
|
||||||
import android.telephony.CellSignalStrength
|
|
||||||
import android.telephony.CellSignalStrengthCdma
|
|
||||||
import android.telephony.ServiceState
|
|
||||||
import android.telephony.SignalStrength
|
|
||||||
import android.telephony.SubscriptionInfo
|
import android.telephony.SubscriptionInfo
|
||||||
import android.telephony.SubscriptionManager
|
import android.telephony.SubscriptionManager
|
||||||
import android.telephony.TelephonyCallback
|
import android.telephony.TelephonyCallback
|
||||||
import android.telephony.TelephonyDisplayInfo
|
|
||||||
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
|
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
|
||||||
import com.android.systemui.dagger.qualifiers.Application
|
|
||||||
import com.android.systemui.dagger.qualifiers.Background
|
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
|
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
|
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
|
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
|
|
||||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
|
||||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
|
|
||||||
import com.android.systemui.util.settings.GlobalSettings
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.asExecutor
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.merge
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
|
* Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
|
||||||
@@ -80,183 +49,3 @@ interface MobileConnectionRepository {
|
|||||||
*/
|
*/
|
||||||
val isDefaultDataSubscription: StateFlow<Boolean>
|
val isDefaultDataSubscription: StateFlow<Boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
class MobileConnectionRepositoryImpl(
|
|
||||||
private val context: Context,
|
|
||||||
private val subId: Int,
|
|
||||||
private val telephonyManager: TelephonyManager,
|
|
||||||
private val globalSettings: GlobalSettings,
|
|
||||||
defaultDataSubId: StateFlow<Int>,
|
|
||||||
globalMobileDataSettingChangedEvent: Flow<Unit>,
|
|
||||||
bgDispatcher: CoroutineDispatcher,
|
|
||||||
logger: ConnectivityPipelineLogger,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
) : MobileConnectionRepository {
|
|
||||||
init {
|
|
||||||
if (telephonyManager.subscriptionId != subId) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"TelephonyManager should be created with subId($subId). " +
|
|
||||||
"Found ${telephonyManager.subscriptionId} instead."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
|
||||||
|
|
||||||
override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
|
|
||||||
var state = MobileSubscriptionModel()
|
|
||||||
conflatedCallbackFlow {
|
|
||||||
// TODO (b/240569788): log all of these into the connectivity logger
|
|
||||||
val callback =
|
|
||||||
object :
|
|
||||||
TelephonyCallback(),
|
|
||||||
TelephonyCallback.ServiceStateListener,
|
|
||||||
TelephonyCallback.SignalStrengthsListener,
|
|
||||||
TelephonyCallback.DataConnectionStateListener,
|
|
||||||
TelephonyCallback.DataActivityListener,
|
|
||||||
TelephonyCallback.CarrierNetworkListener,
|
|
||||||
TelephonyCallback.DisplayInfoListener {
|
|
||||||
override fun onServiceStateChanged(serviceState: ServiceState) {
|
|
||||||
state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
|
|
||||||
trySend(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
|
|
||||||
val cdmaLevel =
|
|
||||||
signalStrength
|
|
||||||
.getCellSignalStrengths(CellSignalStrengthCdma::class.java)
|
|
||||||
.let { strengths ->
|
|
||||||
if (!strengths.isEmpty()) {
|
|
||||||
strengths[0].level
|
|
||||||
} else {
|
|
||||||
CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val primaryLevel = signalStrength.level
|
|
||||||
|
|
||||||
state =
|
|
||||||
state.copy(
|
|
||||||
cdmaLevel = cdmaLevel,
|
|
||||||
primaryLevel = primaryLevel,
|
|
||||||
isGsm = signalStrength.isGsm,
|
|
||||||
)
|
|
||||||
trySend(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDataConnectionStateChanged(
|
|
||||||
dataState: Int,
|
|
||||||
networkType: Int
|
|
||||||
) {
|
|
||||||
state =
|
|
||||||
state.copy(dataConnectionState = dataState.toDataConnectionType())
|
|
||||||
trySend(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDataActivity(direction: Int) {
|
|
||||||
state = state.copy(dataActivityDirection = direction)
|
|
||||||
trySend(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCarrierNetworkChange(active: Boolean) {
|
|
||||||
state = state.copy(carrierNetworkChangeActive = active)
|
|
||||||
trySend(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisplayInfoChanged(
|
|
||||||
telephonyDisplayInfo: TelephonyDisplayInfo
|
|
||||||
) {
|
|
||||||
val networkType =
|
|
||||||
if (
|
|
||||||
telephonyDisplayInfo.overrideNetworkType ==
|
|
||||||
OVERRIDE_NETWORK_TYPE_NONE
|
|
||||||
) {
|
|
||||||
DefaultNetworkType(telephonyDisplayInfo.networkType)
|
|
||||||
} else {
|
|
||||||
OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
|
|
||||||
}
|
|
||||||
state = state.copy(resolvedNetworkType = networkType)
|
|
||||||
trySend(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
|
|
||||||
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
|
|
||||||
}
|
|
||||||
.onEach { telephonyCallbackEvent.tryEmit(Unit) }
|
|
||||||
.logOutputChange(logger, "MobileSubscriptionModel")
|
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Produces whenever the mobile data setting changes for this subId */
|
|
||||||
private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
|
|
||||||
val observer =
|
|
||||||
object : ContentObserver(null) {
|
|
||||||
override fun onChange(selfChange: Boolean) {
|
|
||||||
trySend(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalSettings.registerContentObserver(
|
|
||||||
globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
|
|
||||||
/* notifyForDescendants */ true,
|
|
||||||
observer
|
|
||||||
)
|
|
||||||
|
|
||||||
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There are a few cases where we will need to poll [TelephonyManager] so we can update some
|
|
||||||
* internal state where callbacks aren't provided. Any of those events should be merged into
|
|
||||||
* this flow, which can be used to trigger the polling.
|
|
||||||
*/
|
|
||||||
private val telephonyPollingEvent: Flow<Unit> =
|
|
||||||
merge(
|
|
||||||
telephonyCallbackEvent,
|
|
||||||
localMobileDataSettingChangedEvent,
|
|
||||||
globalMobileDataSettingChangedEvent,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val dataEnabled: StateFlow<Boolean> =
|
|
||||||
telephonyPollingEvent
|
|
||||||
.mapLatest { dataConnectionAllowed() }
|
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
|
|
||||||
|
|
||||||
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
|
|
||||||
|
|
||||||
override val isDefaultDataSubscription: StateFlow<Boolean> =
|
|
||||||
defaultDataSubId
|
|
||||||
.mapLatest { it == subId }
|
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
|
|
||||||
|
|
||||||
class Factory
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
private val context: Context,
|
|
||||||
private val telephonyManager: TelephonyManager,
|
|
||||||
private val logger: ConnectivityPipelineLogger,
|
|
||||||
private val globalSettings: GlobalSettings,
|
|
||||||
@Background private val bgDispatcher: CoroutineDispatcher,
|
|
||||||
@Application private val scope: CoroutineScope,
|
|
||||||
) {
|
|
||||||
fun build(
|
|
||||||
subId: Int,
|
|
||||||
defaultDataSubId: StateFlow<Int>,
|
|
||||||
globalMobileDataSettingChangedEvent: Flow<Unit>,
|
|
||||||
): MobileConnectionRepository {
|
|
||||||
return MobileConnectionRepositoryImpl(
|
|
||||||
context,
|
|
||||||
subId,
|
|
||||||
telephonyManager.createForSubscriptionId(subId),
|
|
||||||
globalSettings,
|
|
||||||
defaultDataSubId,
|
|
||||||
globalMobileDataSettingChangedEvent,
|
|
||||||
bgDispatcher,
|
|
||||||
logger,
|
|
||||||
scope,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,53 +16,14 @@
|
|||||||
|
|
||||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.database.ContentObserver
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.ConnectivityManager.NetworkCallback
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.Global.MOBILE_DATA
|
|
||||||
import android.telephony.CarrierConfigManager
|
|
||||||
import android.telephony.SubscriptionInfo
|
import android.telephony.SubscriptionInfo
|
||||||
import android.telephony.SubscriptionManager
|
import android.telephony.SubscriptionManager
|
||||||
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
|
||||||
import android.telephony.TelephonyCallback
|
|
||||||
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
|
|
||||||
import android.telephony.TelephonyManager
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import com.android.internal.telephony.PhoneConstants
|
|
||||||
import com.android.settingslib.mobile.MobileMappings
|
import com.android.settingslib.mobile.MobileMappings
|
||||||
import com.android.settingslib.mobile.MobileMappings.Config
|
import com.android.settingslib.mobile.MobileMappings.Config
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
|
||||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
|
||||||
import com.android.systemui.dagger.qualifiers.Application
|
|
||||||
import com.android.systemui.dagger.qualifiers.Background
|
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
|
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
|
||||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
|
||||||
import com.android.systemui.util.settings.GlobalSettings
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.asExecutor
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.merge
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repo for monitoring the complete active subscription info list, to be consumed and filtered based
|
* Repo for monitoring the complete active subscription info list, to be consumed and filtered based
|
||||||
@@ -90,202 +51,3 @@ interface MobileConnectionsRepository {
|
|||||||
/** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
|
/** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
|
||||||
val globalMobileDataSettingChangedEvent: Flow<Unit>
|
val globalMobileDataSettingChangedEvent: Flow<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@SysUISingleton
|
|
||||||
class MobileConnectionsRepositoryImpl
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
private val connectivityManager: ConnectivityManager,
|
|
||||||
private val subscriptionManager: SubscriptionManager,
|
|
||||||
private val telephonyManager: TelephonyManager,
|
|
||||||
private val logger: ConnectivityPipelineLogger,
|
|
||||||
broadcastDispatcher: BroadcastDispatcher,
|
|
||||||
private val globalSettings: GlobalSettings,
|
|
||||||
private val context: Context,
|
|
||||||
@Background private val bgDispatcher: CoroutineDispatcher,
|
|
||||||
@Application private val scope: CoroutineScope,
|
|
||||||
private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
|
|
||||||
) : MobileConnectionsRepository {
|
|
||||||
private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State flow that emits the set of mobile data subscriptions, each represented by its own
|
|
||||||
* [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
|
|
||||||
* info object, but for now we keep track of the infos themselves.
|
|
||||||
*/
|
|
||||||
override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
|
|
||||||
conflatedCallbackFlow {
|
|
||||||
val callback =
|
|
||||||
object : SubscriptionManager.OnSubscriptionsChangedListener() {
|
|
||||||
override fun onSubscriptionsChanged() {
|
|
||||||
trySend(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionManager.addOnSubscriptionsChangedListener(
|
|
||||||
bgDispatcher.asExecutor(),
|
|
||||||
callback,
|
|
||||||
)
|
|
||||||
|
|
||||||
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
|
|
||||||
}
|
|
||||||
.mapLatest { fetchSubscriptionsList() }
|
|
||||||
.onEach { infos -> dropUnusedReposFromCache(infos) }
|
|
||||||
.stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
|
|
||||||
|
|
||||||
/** StateFlow that keeps track of the current active mobile data subscription */
|
|
||||||
override val activeMobileDataSubscriptionId: StateFlow<Int> =
|
|
||||||
conflatedCallbackFlow {
|
|
||||||
val callback =
|
|
||||||
object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
|
|
||||||
override fun onActiveDataSubscriptionIdChanged(subId: Int) {
|
|
||||||
trySend(subId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
|
|
||||||
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
|
|
||||||
}
|
|
||||||
.stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
|
|
||||||
|
|
||||||
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
|
|
||||||
MutableSharedFlow(extraBufferCapacity = 1)
|
|
||||||
|
|
||||||
override val defaultDataSubId: StateFlow<Int> =
|
|
||||||
broadcastDispatcher
|
|
||||||
.broadcastFlow(
|
|
||||||
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
|
||||||
) { intent, _ ->
|
|
||||||
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
|
|
||||||
}
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
|
|
||||||
.stateIn(
|
|
||||||
scope,
|
|
||||||
SharingStarted.WhileSubscribed(),
|
|
||||||
SubscriptionManager.getDefaultDataSubscriptionId()
|
|
||||||
)
|
|
||||||
|
|
||||||
private val carrierConfigChangedEvent =
|
|
||||||
broadcastDispatcher.broadcastFlow(
|
|
||||||
IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Config] is an object that tracks relevant configuration flags for a given subscription ID.
|
|
||||||
* In the case of [MobileMappings], it's hard-coded to check the default data subscription's
|
|
||||||
* config, so this will apply to every icon that we care about.
|
|
||||||
*
|
|
||||||
* Relevant bits in the config are things like
|
|
||||||
* [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
|
|
||||||
*
|
|
||||||
* This flow will produce whenever the default data subscription or the carrier config changes.
|
|
||||||
*/
|
|
||||||
override val defaultDataSubRatConfig: StateFlow<Config> =
|
|
||||||
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
|
|
||||||
.mapLatest { Config.readConfig(context) }
|
|
||||||
.stateIn(
|
|
||||||
scope,
|
|
||||||
SharingStarted.WhileSubscribed(),
|
|
||||||
initialValue = Config.readConfig(context)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
|
|
||||||
if (!isValidSubId(subId)) {
|
|
||||||
throw IllegalArgumentException(
|
|
||||||
"subscriptionId $subId is not in the list of valid subscriptions"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return subIdRepositoryCache[subId]
|
|
||||||
?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
|
|
||||||
* connection repositories also observe the URI for [MOBILE_DATA] + subId.
|
|
||||||
*/
|
|
||||||
override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
|
|
||||||
val observer =
|
|
||||||
object : ContentObserver(null) {
|
|
||||||
override fun onChange(selfChange: Boolean) {
|
|
||||||
trySend(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalSettings.registerContentObserver(
|
|
||||||
globalSettings.getUriFor(MOBILE_DATA),
|
|
||||||
true,
|
|
||||||
observer
|
|
||||||
)
|
|
||||||
|
|
||||||
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
|
|
||||||
conflatedCallbackFlow {
|
|
||||||
val callback =
|
|
||||||
object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
// Send a disconnected model when lost. Maybe should create a sealed
|
|
||||||
// type or null here?
|
|
||||||
trySend(MobileConnectivityModel())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(
|
|
||||||
network: Network,
|
|
||||||
caps: NetworkCapabilities
|
|
||||||
) {
|
|
||||||
trySend(
|
|
||||||
MobileConnectivityModel(
|
|
||||||
isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
|
|
||||||
isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectivityManager.registerDefaultNetworkCallback(callback)
|
|
||||||
|
|
||||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
|
||||||
}
|
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
|
|
||||||
|
|
||||||
private fun isValidSubId(subId: Int): Boolean {
|
|
||||||
subscriptionsFlow.value.forEach {
|
|
||||||
if (it.subscriptionId == subId) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
|
|
||||||
|
|
||||||
private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
|
|
||||||
return mobileConnectionRepositoryFactory.build(
|
|
||||||
subId,
|
|
||||||
defaultDataSubId,
|
|
||||||
globalMobileDataSettingChangedEvent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
|
|
||||||
// Remove any connection repository from the cache that isn't in the new set of IDs. They
|
|
||||||
// will get garbage collected once their subscribers go away
|
|
||||||
val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
|
|
||||||
|
|
||||||
subIdRepositoryCache.keys.forEach {
|
|
||||||
if (!currentValidSubscriptionIds.contains(it)) {
|
|
||||||
subIdRepositoryCache.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
|
|
||||||
withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
|
||||||
|
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -40,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionS
|
|||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
|
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
|
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
|
||||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
|
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
|
||||||
|
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
|
||||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
||||||
import com.android.systemui.util.mockito.any
|
import com.android.systemui.util.mockito.any
|
||||||
import com.android.systemui.util.mockito.argumentCaptor
|
import com.android.systemui.util.mockito.argumentCaptor
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
Reference in New Issue
Block a user