[SB refactor] Track the default subscription and its connection status
This CL adds the ability for `MobileIconsInteractor` to track which `MobileConnectionRepository` represents the active data subscription connection, and tracks its disabled status. In the case of DSDS, for instance, data will not be enabled on the stanby SIM, but we will only want to show the disconnected state (the exclamation mark on the triangle) when the active mobile data subscription has data turned off. Also, added `ContentObserver`s for the `MOBILE_DATA` setting, which is used to trigger telephony polling events. This mimics behavior from the old `MobileSignalController` and help us to update the icon once the data enabled state changes. Test: atest MobileConnectionsRepositoryTest MobileConnectionRepositoryTest MobileIconsInteractorTest MobileIconInteractorTest MobileIconViewModelTest Bug: 240492102 Change-Id: I6f614d615930dca1d0c69ef361ecc05ce394dbfc
This commit is contained in:
@@ -16,11 +16,15 @@
|
||||
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.provider.Settings.Global
|
||||
import android.telephony.CellSignalStrength
|
||||
import android.telephony.CellSignalStrengthCdma
|
||||
import android.telephony.ServiceState
|
||||
import android.telephony.SignalStrength
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyDisplayInfo
|
||||
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
|
||||
@@ -34,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetwork
|
||||
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
|
||||
@@ -45,6 +50,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
@@ -66,13 +72,22 @@ interface MobileConnectionRepository {
|
||||
val subscriptionModelFlow: Flow<MobileSubscriptionModel>
|
||||
/** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
|
||||
val dataEnabled: Flow<Boolean>
|
||||
/**
|
||||
* True if this connection represents the default subscription per
|
||||
* [SubscriptionManager.getDefaultDataSubscriptionId]
|
||||
*/
|
||||
val isDefaultDataSubscription: Flow<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: Flow<Int>,
|
||||
globalMobileDataSettingChangedEvent: Flow<Unit>,
|
||||
bgDispatcher: CoroutineDispatcher,
|
||||
logger: ConnectivityPipelineLogger,
|
||||
scope: CoroutineScope,
|
||||
@@ -169,29 +184,66 @@ class MobileConnectionRepositoryImpl(
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
|
||||
}
|
||||
|
||||
private val telephonyCallbackEvent = subscriptionModelFlow.map {}
|
||||
|
||||
/** 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> = subscriptionModelFlow.map {}
|
||||
private val telephonyPollingEvent: Flow<Unit> =
|
||||
merge(
|
||||
telephonyCallbackEvent,
|
||||
localMobileDataSettingChangedEvent,
|
||||
globalMobileDataSettingChangedEvent,
|
||||
)
|
||||
|
||||
override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() }
|
||||
|
||||
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
|
||||
|
||||
override val isDefaultDataSubscription: Flow<Boolean> = defaultDataSubId.map { it == 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): MobileConnectionRepository {
|
||||
fun build(
|
||||
subId: Int,
|
||||
defaultDataSubId: Flow<Int>,
|
||||
globalMobileDataSettingChangedEvent: Flow<Unit>,
|
||||
): MobileConnectionRepository {
|
||||
return MobileConnectionRepositoryImpl(
|
||||
context,
|
||||
subId,
|
||||
telephonyManager.createForSubscriptionId(subId),
|
||||
globalSettings,
|
||||
defaultDataSubId,
|
||||
globalMobileDataSettingChangedEvent,
|
||||
bgDispatcher,
|
||||
logger,
|
||||
scope,
|
||||
|
||||
@@ -18,13 +18,18 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.database.ContentObserver
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.Global.MOBILE_DATA
|
||||
import android.telephony.CarrierConfigManager
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.internal.telephony.PhoneConstants
|
||||
import com.android.settingslib.mobile.MobileMappings
|
||||
import com.android.settingslib.mobile.MobileMappings.Config
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
@@ -33,6 +38,7 @@ import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
||||
import com.android.systemui.util.settings.GlobalSettings
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -40,10 +46,12 @@ 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.combine
|
||||
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
|
||||
@@ -62,8 +70,14 @@ interface MobileConnectionsRepository {
|
||||
/** Observable for [MobileMappings.Config] tracking the defaults */
|
||||
val defaultDataSubRatConfig: StateFlow<Config>
|
||||
|
||||
/** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
|
||||
val defaultDataSubId: StateFlow<Int>
|
||||
|
||||
/** Get or create a repository for the line of service for the given subscription ID */
|
||||
fun getRepoForSubId(subId: Int): MobileConnectionRepository
|
||||
|
||||
/** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
|
||||
val globalMobileDataSettingChangedEvent: Flow<Unit>
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
|
||||
@@ -76,6 +90,7 @@ constructor(
|
||||
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,
|
||||
@@ -121,17 +136,26 @@ constructor(
|
||||
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,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
SharingStarted.WhileSubscribed(),
|
||||
SubscriptionManager.getDefaultDataSubscriptionId()
|
||||
)
|
||||
|
||||
private val defaultDataSubChangedEvent =
|
||||
broadcastDispatcher.broadcastFlow(
|
||||
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
)
|
||||
|
||||
private val carrierConfigChangedEvent =
|
||||
broadcastDispatcher.broadcastFlow(
|
||||
IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
|
||||
@@ -148,9 +172,8 @@ constructor(
|
||||
* This flow will produce whenever the default data subscription or the carrier config changes.
|
||||
*/
|
||||
override val defaultDataSubRatConfig: StateFlow<Config> =
|
||||
combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ ->
|
||||
Config.readConfig(context)
|
||||
}
|
||||
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
|
||||
.mapLatest { Config.readConfig(context) }
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.WhileSubscribed(),
|
||||
@@ -168,6 +191,27 @@ constructor(
|
||||
?: 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) }
|
||||
}
|
||||
|
||||
private fun isValidSubId(subId: Int): Boolean {
|
||||
subscriptionsFlow.value.forEach {
|
||||
if (it.subscriptionId == subId) {
|
||||
@@ -181,7 +225,11 @@ constructor(
|
||||
@VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
|
||||
|
||||
private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
|
||||
return mobileConnectionRepositoryFactory.build(subId)
|
||||
return mobileConnectionRepositoryFactory.build(
|
||||
subId,
|
||||
defaultDataSubId,
|
||||
globalMobileDataSettingChangedEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
|
||||
|
||||
@@ -29,6 +29,10 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface MobileIconInteractor {
|
||||
// TODO(b/256839546): clarify naming of default vs active
|
||||
/** True if we want to consider the data connection enabled */
|
||||
val isDefaultDataEnabled: Flow<Boolean>
|
||||
|
||||
/** Observable for the data enabled state of this connection */
|
||||
val isDataEnabled: Flow<Boolean>
|
||||
|
||||
@@ -43,13 +47,11 @@ interface MobileIconInteractor {
|
||||
|
||||
/** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
|
||||
val numberOfLevels: Flow<Int>
|
||||
|
||||
/** True when we want to draw an icon that makes room for the exclamation mark */
|
||||
val cutOut: Flow<Boolean>
|
||||
}
|
||||
|
||||
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
|
||||
class MobileIconInteractorImpl(
|
||||
defaultSubscriptionHasDataEnabled: Flow<Boolean>,
|
||||
defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
|
||||
defaultMobileIconGroup: Flow<MobileIconGroup>,
|
||||
mobileMappingsProxy: MobileMappingsProxy,
|
||||
@@ -59,6 +61,8 @@ class MobileIconInteractorImpl(
|
||||
|
||||
override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled
|
||||
|
||||
override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
|
||||
|
||||
/** Observable for the current RAT indicator icon ([MobileIconGroup]) */
|
||||
override val networkTypeIconGroup: Flow<MobileIconGroup> =
|
||||
combine(
|
||||
@@ -91,8 +95,4 @@ class MobileIconInteractorImpl(
|
||||
* once it's wired up inside of [CarrierConfigTracker]
|
||||
*/
|
||||
override val numberOfLevels: Flow<Int> = flowOf(4)
|
||||
|
||||
/** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */
|
||||
// TODO: find a better name for this?
|
||||
override val cutOut: Flow<Boolean> = flowOf(false)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
|
||||
import android.telephony.CarrierConfigManager
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
import com.android.settingslib.SignalIcon.MobileIconGroup
|
||||
import com.android.settingslib.mobile.TelephonyIcons
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
@@ -35,6 +36,8 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@@ -51,6 +54,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||
interface MobileIconsInteractor {
|
||||
/** List of subscriptions, potentially filtered for CBRS */
|
||||
val filteredSubscriptions: Flow<List<SubscriptionInfo>>
|
||||
/** True if the active mobile data subscription has data enabled */
|
||||
val activeDataConnectionHasDataEnabled: Flow<Boolean>
|
||||
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
|
||||
val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
|
||||
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
|
||||
@@ -79,6 +84,18 @@ constructor(
|
||||
private val activeMobileDataSubscriptionId =
|
||||
mobileConnectionsRepo.activeMobileDataSubscriptionId
|
||||
|
||||
private val activeMobileDataConnectionRepo: Flow<MobileConnectionRepository?> =
|
||||
activeMobileDataSubscriptionId.map { activeId ->
|
||||
if (activeId == INVALID_SUBSCRIPTION_ID) {
|
||||
null
|
||||
} else {
|
||||
mobileConnectionsRepo.getRepoForSubId(activeId)
|
||||
}
|
||||
}
|
||||
|
||||
override val activeDataConnectionHasDataEnabled: Flow<Boolean> =
|
||||
activeMobileDataConnectionRepo.flatMapLatest { it?.dataEnabled ?: flowOf(false) }
|
||||
|
||||
private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
|
||||
mobileConnectionsRepo.subscriptionsFlow
|
||||
|
||||
@@ -146,6 +163,7 @@ constructor(
|
||||
/** Vends out new [MobileIconInteractor] for a particular subId */
|
||||
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
|
||||
MobileIconInteractorImpl(
|
||||
activeDataConnectionHasDataEnabled,
|
||||
defaultMobileIconMapping,
|
||||
defaultMobileIconGroup,
|
||||
mobileMappingsProxy,
|
||||
|
||||
@@ -24,10 +24,12 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc
|
||||
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
|
||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
|
||||
/**
|
||||
* View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
|
||||
@@ -39,25 +41,31 @@ import kotlinx.coroutines.flow.flowOf
|
||||
*
|
||||
* TODO: figure out where carrier merged and VCN models go (probably here?)
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MobileIconViewModel
|
||||
constructor(
|
||||
val subscriptionId: Int,
|
||||
iconInteractor: MobileIconInteractor,
|
||||
logger: ConnectivityPipelineLogger,
|
||||
) {
|
||||
/** Whether or not to show the error state of [SignalDrawable] */
|
||||
private val showExclamationMark: Flow<Boolean> =
|
||||
iconInteractor.isDefaultDataEnabled.mapLatest { !it }
|
||||
|
||||
/** An int consumable by [SignalDrawable] for display */
|
||||
var iconId: Flow<Int> =
|
||||
combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) {
|
||||
val iconId: Flow<Int> =
|
||||
combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) {
|
||||
level,
|
||||
numberOfLevels,
|
||||
cutOut ->
|
||||
SignalDrawable.getState(level, numberOfLevels, cutOut)
|
||||
showExclamationMark ->
|
||||
SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.logOutputChange(logger, "iconId($subscriptionId)")
|
||||
|
||||
/** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
|
||||
var networkTypeIcon: Flow<Icon?> =
|
||||
val networkTypeIcon: Flow<Icon?> =
|
||||
combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) {
|
||||
networkTypeIconGroup,
|
||||
isDataEnabled ->
|
||||
@@ -72,5 +80,5 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
var tint: Flow<Int> = flowOf(Color.CYAN)
|
||||
val tint: Flow<Int> = flowOf(Color.CYAN)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ class FakeMobileConnectionRepository : MobileConnectionRepository {
|
||||
private val _dataEnabled = MutableStateFlow(true)
|
||||
override val dataEnabled = _dataEnabled
|
||||
|
||||
private val _isDefaultDataSubscription = MutableStateFlow(true)
|
||||
override val isDefaultDataSubscription = _isDefaultDataSubscription
|
||||
|
||||
fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
|
||||
_subscriptionsModelFlow.value = model
|
||||
}
|
||||
@@ -34,4 +37,8 @@ class FakeMobileConnectionRepository : MobileConnectionRepository {
|
||||
fun setDataEnabled(enabled: Boolean) {
|
||||
_dataEnabled.value = enabled
|
||||
}
|
||||
|
||||
fun setIsDefaultDataSubscription(isDefault: Boolean) {
|
||||
_isDefaultDataSubscription.value = isDefault
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
import com.android.settingslib.mobile.MobileMappings.Config
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -26,18 +26,23 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository {
|
||||
private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
|
||||
override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
|
||||
|
||||
private val _activeMobileDataSubscriptionId =
|
||||
MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
|
||||
private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
|
||||
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
|
||||
|
||||
private val _defaultDataSubRatConfig = MutableStateFlow(Config())
|
||||
override val defaultDataSubRatConfig = _defaultDataSubRatConfig
|
||||
|
||||
private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
|
||||
override val defaultDataSubId = _defaultDataSubId
|
||||
|
||||
private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
|
||||
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
|
||||
return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
|
||||
}
|
||||
|
||||
private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
|
||||
override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent
|
||||
|
||||
fun setSubscriptions(subs: List<SubscriptionInfo>) {
|
||||
_subscriptionsFlow.value = subs
|
||||
}
|
||||
@@ -46,6 +51,14 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository {
|
||||
_defaultDataSubRatConfig.value = config
|
||||
}
|
||||
|
||||
fun setDefaultDataSubId(id: Int) {
|
||||
_defaultDataSubId.value = id
|
||||
}
|
||||
|
||||
suspend fun triggerGlobalMobileDataSettingChangedEvent() {
|
||||
_globalMobileDataSettingChangedEvent.emit(Unit)
|
||||
}
|
||||
|
||||
fun setActiveMobileDataSubscriptionId(subId: Int) {
|
||||
_activeMobileDataSubscriptionId.value = subId
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.os.UserHandle
|
||||
import android.provider.Settings
|
||||
import android.telephony.CellSignalStrengthCdma
|
||||
import android.telephony.ServiceState
|
||||
import android.telephony.SignalStrength
|
||||
@@ -42,6 +44,7 @@ import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -67,16 +70,23 @@ class MobileConnectionRepositoryTest : SysuiTestCase() {
|
||||
@Mock private lateinit var logger: ConnectivityPipelineLogger
|
||||
|
||||
private val scope = CoroutineScope(IMMEDIATE)
|
||||
private val globalSettings = FakeSettings()
|
||||
private val connectionsRepo = FakeMobileConnectionsRepository()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
globalSettings.userId = UserHandle.USER_ALL
|
||||
whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
|
||||
|
||||
underTest =
|
||||
MobileConnectionRepositoryImpl(
|
||||
context,
|
||||
SUB_1_ID,
|
||||
telephonyManager,
|
||||
globalSettings,
|
||||
connectionsRepo.defaultDataSubId,
|
||||
connectionsRepo.globalMobileDataSettingChangedEvent,
|
||||
IMMEDIATE,
|
||||
logger,
|
||||
scope,
|
||||
@@ -315,6 +325,57 @@ class MobileConnectionRepositoryTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isDefaultDataSubscription_isDefault() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
connectionsRepo.setDefaultDataSubId(SUB_1_ID)
|
||||
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
|
||||
|
||||
assertThat(latest).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isDefaultDataSubscription_isNotDefault() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
// Our subId is SUB_1_ID
|
||||
connectionsRepo.setDefaultDataSubId(123)
|
||||
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
|
||||
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID"
|
||||
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
|
||||
|
||||
// We don't read the setting directly, we query telephony when changes happen
|
||||
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
|
||||
globalSettings.putInt(subIdSettingName, 0)
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
|
||||
globalSettings.putInt(subIdSettingName, 1)
|
||||
assertThat(latest).isTrue()
|
||||
|
||||
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
|
||||
globalSettings.putInt(subIdSettingName, 0)
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun getTelephonyCallbacks(): List<TelephonyCallback> {
|
||||
val callbackCaptor = argumentCaptor<TelephonyCallback>()
|
||||
Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
|
||||
|
||||
@@ -16,26 +16,27 @@
|
||||
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.telephony.PhoneConstants
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.nullable
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.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.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -43,7 +44,6 @@ import org.junit.After
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
@@ -57,29 +57,21 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
|
||||
@Mock private lateinit var subscriptionManager: SubscriptionManager
|
||||
@Mock private lateinit var telephonyManager: TelephonyManager
|
||||
@Mock private lateinit var logger: ConnectivityPipelineLogger
|
||||
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
|
||||
|
||||
private val scope = CoroutineScope(IMMEDIATE)
|
||||
private val globalSettings = FakeSettings()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(
|
||||
broadcastDispatcher.broadcastFlow(
|
||||
any(),
|
||||
nullable(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
nullable(),
|
||||
)
|
||||
)
|
||||
.thenReturn(flowOf(Unit))
|
||||
|
||||
underTest =
|
||||
MobileConnectionsRepositoryImpl(
|
||||
subscriptionManager,
|
||||
telephonyManager,
|
||||
logger,
|
||||
broadcastDispatcher,
|
||||
fakeBroadcastDispatcher,
|
||||
globalSettings,
|
||||
context,
|
||||
IMMEDIATE,
|
||||
scope,
|
||||
@@ -214,6 +206,53 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDefaultDataSubId_updatesOnBroadcast() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Int? = null
|
||||
val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
|
||||
|
||||
fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
|
||||
receiver.onReceive(
|
||||
context,
|
||||
Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
.putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(latest).isEqualTo(SUB_2_ID)
|
||||
|
||||
fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
|
||||
receiver.onReceive(
|
||||
context,
|
||||
Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
.putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(latest).isEqualTo(SUB_1_ID)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var produced = false
|
||||
val job =
|
||||
underTest.globalMobileDataSettingChangedEvent
|
||||
.onEach { produced = true }
|
||||
.launchIn(this)
|
||||
|
||||
assertThat(produced).isFalse()
|
||||
|
||||
globalSettings.putInt(Settings.Global.MOBILE_DATA, 0)
|
||||
|
||||
assertThat(produced).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
|
||||
val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
|
||||
verify(subscriptionManager)
|
||||
|
||||
@@ -31,15 +31,15 @@ class FakeMobileIconInteractor : MobileIconInteractor {
|
||||
private val _isDataEnabled = MutableStateFlow(true)
|
||||
override val isDataEnabled = _isDataEnabled
|
||||
|
||||
private val _isDefaultDataEnabled = MutableStateFlow(true)
|
||||
override val isDefaultDataEnabled = _isDefaultDataEnabled
|
||||
|
||||
private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
|
||||
override val level = _level
|
||||
|
||||
private val _numberOfLevels = MutableStateFlow(4)
|
||||
override val numberOfLevels = _numberOfLevels
|
||||
|
||||
private val _cutOut = MutableStateFlow(false)
|
||||
override val cutOut = _cutOut
|
||||
|
||||
fun setIconGroup(group: SignalIcon.MobileIconGroup) {
|
||||
_iconGroup.value = group
|
||||
}
|
||||
@@ -52,6 +52,10 @@ class FakeMobileIconInteractor : MobileIconInteractor {
|
||||
_isDataEnabled.value = enabled
|
||||
}
|
||||
|
||||
fun setIsDefaultDataEnabled(disabled: Boolean) {
|
||||
_isDefaultDataEnabled.value = disabled
|
||||
}
|
||||
|
||||
fun setLevel(level: Int) {
|
||||
_level.value = level
|
||||
}
|
||||
@@ -59,8 +63,4 @@ class FakeMobileIconInteractor : MobileIconInteractor {
|
||||
fun setNumberOfLevels(num: Int) {
|
||||
_numberOfLevels.value = num
|
||||
}
|
||||
|
||||
fun setCutOut(cutOut: Boolean) {
|
||||
_cutOut.value = cutOut
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ import com.android.settingslib.mobile.TelephonyIcons
|
||||
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) :
|
||||
MobileIconsInteractor {
|
||||
class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor {
|
||||
val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
|
||||
val LTE_KEY = mobileMappings.toIconKey(LTE)
|
||||
val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
|
||||
@@ -49,6 +48,9 @@ class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy)
|
||||
private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
|
||||
override val filteredSubscriptions = _filteredSubscriptions
|
||||
|
||||
private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
|
||||
override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
|
||||
|
||||
private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
|
||||
override val defaultMobileIconMapping = _defaultMobileIconMapping
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class MobileIconInteractorTest : SysuiTestCase() {
|
||||
fun setUp() {
|
||||
underTest =
|
||||
MobileIconInteractorImpl(
|
||||
mobileIconsInteractor.activeDataConnectionHasDataEnabled,
|
||||
mobileIconsInteractor.defaultMobileIconMapping,
|
||||
mobileIconsInteractor.defaultMobileIconGroup,
|
||||
mobileMappingsProxy,
|
||||
@@ -196,6 +197,21 @@ class MobileIconInteractorTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_isDefaultDataEnabled_matchesParent() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDefaultDataEnabled.onEach { latest = it }.launchIn(this)
|
||||
|
||||
mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
|
||||
assertThat(latest).isTrue()
|
||||
|
||||
mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
|
||||
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
|
||||
@@ -32,6 +33,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.yield
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -168,6 +170,51 @@ class MobileIconsInteractorTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeDataConnection_turnedOn() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
CONNECTION_1.setDataEnabled(true)
|
||||
var latest: Boolean? = null
|
||||
val job =
|
||||
underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
|
||||
|
||||
assertThat(latest).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeDataConnection_turnedOff() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
CONNECTION_1.setDataEnabled(true)
|
||||
var latest: Boolean? = null
|
||||
val job =
|
||||
underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
|
||||
|
||||
CONNECTION_1.setDataEnabled(false)
|
||||
yield()
|
||||
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeDataConnection_invalidSubId() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job =
|
||||
underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
|
||||
|
||||
connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
|
||||
yield()
|
||||
|
||||
// An invalid active subId should tell us that data is off
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class MobileIconViewModelTest : SysuiTestCase() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
interactor.apply {
|
||||
setLevel(1)
|
||||
setCutOut(false)
|
||||
setIsDefaultDataEnabled(true)
|
||||
setIconGroup(THREE_G)
|
||||
setIsEmergencyOnly(false)
|
||||
setNumberOfLevels(4)
|
||||
@@ -59,8 +59,23 @@ class MobileIconViewModelTest : SysuiTestCase() {
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Int? = null
|
||||
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
|
||||
val expected = defaultSignal()
|
||||
|
||||
assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false))
|
||||
assertThat(latest).isEqualTo(expected)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iconId_cutout_whenDefaultDataDisabled() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
interactor.setIsDefaultDataEnabled(false)
|
||||
|
||||
var latest: Int? = null
|
||||
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
|
||||
val expected = defaultSignal(level = 1, connected = false)
|
||||
|
||||
assertThat(latest).isEqualTo(expected)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
@@ -119,6 +134,14 @@ class MobileIconViewModelTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
/** Convenience constructor for these tests */
|
||||
private fun defaultSignal(
|
||||
level: Int = 1,
|
||||
connected: Boolean = true,
|
||||
): Int {
|
||||
return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
private const val SUB_1_ID = 1
|
||||
|
||||
Reference in New Issue
Block a user