Merge changes I37ab7767,I13890cdc,Idd5fa020,I6f614d61 into tm-qpr-dev
* changes: [SB refactor] Wire up DataConnectionState to the view model [SB refactor] Add connectivity tracking to the mobile pipeline [SB refactor] Upgrade most data sources to `StateFlow` [SB refactor] Track the default subscription and its connection status
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
|
||||
/** Provides information about a mobile network connection */
|
||||
data class MobileConnectivityModel(
|
||||
/** Whether mobile is the connected transport see [NetworkCapabilities.TRANSPORT_CELLULAR] */
|
||||
val isConnected: Boolean = false,
|
||||
/** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
|
||||
val isValidated: Boolean = false,
|
||||
)
|
||||
@@ -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
|
||||
@@ -42,9 +47,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.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
@@ -65,14 +73,23 @@ interface MobileConnectionRepository {
|
||||
*/
|
||||
val subscriptionModelFlow: Flow<MobileSubscriptionModel>
|
||||
/** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
|
||||
val dataEnabled: Flow<Boolean>
|
||||
val dataEnabled: StateFlow<Boolean>
|
||||
/**
|
||||
* True if this connection represents the default subscription per
|
||||
* [SubscriptionManager.getDefaultDataSubscriptionId]
|
||||
*/
|
||||
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,
|
||||
@@ -86,6 +103,8 @@ class MobileConnectionRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
|
||||
override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
|
||||
var state = MobileSubscriptionModel()
|
||||
conflatedCallbackFlow {
|
||||
@@ -165,33 +184,75 @@ class MobileConnectionRepositoryImpl(
|
||||
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> = subscriptionModelFlow.map {}
|
||||
private val telephonyPollingEvent: Flow<Unit> =
|
||||
merge(
|
||||
telephonyCallbackEvent,
|
||||
localMobileDataSettingChangedEvent,
|
||||
globalMobileDataSettingChangedEvent,
|
||||
)
|
||||
|
||||
override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() }
|
||||
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): MobileConnectionRepository {
|
||||
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,15 +16,27 @@
|
||||
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.database.ContentObserver
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.Global.MOBILE_DATA
|
||||
import android.telephony.CarrierConfigManager
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.internal.telephony.PhoneConstants
|
||||
import com.android.settingslib.mobile.MobileMappings
|
||||
import com.android.settingslib.mobile.MobileMappings.Config
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
@@ -32,7 +44,9 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
|
||||
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
|
||||
import com.android.systemui.util.settings.GlobalSettings
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -40,10 +54,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
|
||||
@@ -57,13 +73,22 @@ interface MobileConnectionsRepository {
|
||||
val subscriptionsFlow: Flow<List<SubscriptionInfo>>
|
||||
|
||||
/** Observable for the subscriptionId of the current mobile data connection */
|
||||
val activeMobileDataSubscriptionId: Flow<Int>
|
||||
val activeMobileDataSubscriptionId: StateFlow<Int>
|
||||
|
||||
/** Observable for [MobileMappings.Config] tracking the defaults */
|
||||
val defaultDataSubRatConfig: StateFlow<Config>
|
||||
|
||||
/** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
|
||||
val defaultDataSubId: StateFlow<Int>
|
||||
|
||||
/** The current connectivity status for the default mobile network connection */
|
||||
val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
|
||||
|
||||
/** 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")
|
||||
@@ -72,10 +97,12 @@ interface MobileConnectionsRepository {
|
||||
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,
|
||||
@@ -121,17 +148,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 +184,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 +203,57 @@ 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) }
|
||||
}
|
||||
|
||||
@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) {
|
||||
@@ -181,7 +267,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>) {
|
||||
|
||||
@@ -26,7 +26,6 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
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.mapLatest
|
||||
@@ -40,7 +39,7 @@ import kotlinx.coroutines.withContext
|
||||
*/
|
||||
interface UserSetupRepository {
|
||||
/** Observable tracking [DeviceProvisionedController.isUserSetup] */
|
||||
val isUserSetupFlow: Flow<Boolean>
|
||||
val isUserSetupFlow: StateFlow<Boolean>
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
|
||||
|
||||
@@ -18,81 +18,109 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
|
||||
|
||||
import android.telephony.CarrierConfigManager
|
||||
import com.android.settingslib.SignalIcon.MobileIconGroup
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
|
||||
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
|
||||
import com.android.systemui.util.CarrierConfigTracker
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
interface MobileIconInteractor {
|
||||
/** Only true if mobile is the default transport but is not validated, otherwise false */
|
||||
val isDefaultConnectionFailed: StateFlow<Boolean>
|
||||
|
||||
/** True when telephony tells us that the data state is CONNECTED */
|
||||
val isDataConnected: StateFlow<Boolean>
|
||||
|
||||
// TODO(b/256839546): clarify naming of default vs active
|
||||
/** True if we want to consider the data connection enabled */
|
||||
val isDefaultDataEnabled: StateFlow<Boolean>
|
||||
|
||||
/** Observable for the data enabled state of this connection */
|
||||
val isDataEnabled: Flow<Boolean>
|
||||
val isDataEnabled: StateFlow<Boolean>
|
||||
|
||||
/** Observable for RAT type (network type) indicator */
|
||||
val networkTypeIconGroup: Flow<MobileIconGroup>
|
||||
val networkTypeIconGroup: StateFlow<MobileIconGroup>
|
||||
|
||||
/** True if this line of service is emergency-only */
|
||||
val isEmergencyOnly: Flow<Boolean>
|
||||
val isEmergencyOnly: StateFlow<Boolean>
|
||||
|
||||
/** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
|
||||
val level: Flow<Int>
|
||||
val level: StateFlow<Int>
|
||||
|
||||
/** 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>
|
||||
val numberOfLevels: StateFlow<Int>
|
||||
}
|
||||
|
||||
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
|
||||
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MobileIconInteractorImpl(
|
||||
defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
|
||||
defaultMobileIconGroup: Flow<MobileIconGroup>,
|
||||
@Application scope: CoroutineScope,
|
||||
defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
|
||||
defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
|
||||
defaultMobileIconGroup: StateFlow<MobileIconGroup>,
|
||||
override val isDefaultConnectionFailed: StateFlow<Boolean>,
|
||||
mobileMappingsProxy: MobileMappingsProxy,
|
||||
connectionRepository: MobileConnectionRepository,
|
||||
) : MobileIconInteractor {
|
||||
private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
|
||||
|
||||
override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled
|
||||
override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
|
||||
|
||||
override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
|
||||
|
||||
/** Observable for the current RAT indicator icon ([MobileIconGroup]) */
|
||||
override val networkTypeIconGroup: Flow<MobileIconGroup> =
|
||||
override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
|
||||
combine(
|
||||
mobileStatusInfo,
|
||||
defaultMobileIconMapping,
|
||||
defaultMobileIconGroup,
|
||||
) { info, mapping, defaultGroup ->
|
||||
val lookupKey =
|
||||
when (val resolved = info.resolvedNetworkType) {
|
||||
is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
|
||||
is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type)
|
||||
}
|
||||
mapping[lookupKey] ?: defaultGroup
|
||||
}
|
||||
|
||||
override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
|
||||
|
||||
override val level: Flow<Int> =
|
||||
mobileStatusInfo.map { mobileModel ->
|
||||
// TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
|
||||
if (mobileModel.isGsm) {
|
||||
mobileModel.primaryLevel
|
||||
} else {
|
||||
mobileModel.cdmaLevel
|
||||
mobileStatusInfo,
|
||||
defaultMobileIconMapping,
|
||||
defaultMobileIconGroup,
|
||||
) { info, mapping, defaultGroup ->
|
||||
val lookupKey =
|
||||
when (val resolved = info.resolvedNetworkType) {
|
||||
is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
|
||||
is OverrideNetworkType ->
|
||||
mobileMappingsProxy.toIconKeyOverride(resolved.type)
|
||||
}
|
||||
mapping[lookupKey] ?: defaultGroup
|
||||
}
|
||||
}
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
|
||||
|
||||
override val isEmergencyOnly: StateFlow<Boolean> =
|
||||
mobileStatusInfo
|
||||
.mapLatest { it.isEmergencyOnly }
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
override val level: StateFlow<Int> =
|
||||
mobileStatusInfo
|
||||
.mapLatest { mobileModel ->
|
||||
// TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
|
||||
if (mobileModel.isGsm) {
|
||||
mobileModel.primaryLevel
|
||||
} else {
|
||||
mobileModel.cdmaLevel
|
||||
}
|
||||
}
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), 0)
|
||||
|
||||
/**
|
||||
* This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
|
||||
* once it's wired up inside of [CarrierConfigTracker]
|
||||
*/
|
||||
override val numberOfLevels: Flow<Int> = flowOf(4)
|
||||
override val numberOfLevels: StateFlow<Int> = MutableStateFlow(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)
|
||||
override val isDataConnected: StateFlow<Boolean> =
|
||||
mobileStatusInfo
|
||||
.mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected }
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), 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,7 +36,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
@@ -51,12 +54,16 @@ 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: StateFlow<Boolean>
|
||||
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
|
||||
val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
|
||||
val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
|
||||
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
|
||||
val defaultMobileIconGroup: Flow<MobileIconGroup>
|
||||
val defaultMobileIconGroup: StateFlow<MobileIconGroup>
|
||||
/** True only if the default network is mobile, and validation also failed */
|
||||
val isDefaultConnectionFailed: StateFlow<Boolean>
|
||||
/** True once the user has been set up */
|
||||
val isUserSetup: Flow<Boolean>
|
||||
val isUserSetup: StateFlow<Boolean>
|
||||
/**
|
||||
* Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
|
||||
* subId. Will throw if the ID is invalid
|
||||
@@ -79,6 +86,22 @@ constructor(
|
||||
private val activeMobileDataSubscriptionId =
|
||||
mobileConnectionsRepo.activeMobileDataSubscriptionId
|
||||
|
||||
private val activeMobileDataConnectionRepo: StateFlow<MobileConnectionRepository?> =
|
||||
activeMobileDataSubscriptionId
|
||||
.mapLatest { activeId ->
|
||||
if (activeId == INVALID_SUBSCRIPTION_ID) {
|
||||
null
|
||||
} else {
|
||||
mobileConnectionsRepo.getRepoForSubId(activeId)
|
||||
}
|
||||
}
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
|
||||
activeMobileDataConnectionRepo
|
||||
.flatMapLatest { it?.dataEnabled ?: flowOf(false) }
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
|
||||
mobileConnectionsRepo.subscriptionsFlow
|
||||
|
||||
@@ -132,22 +155,40 @@ constructor(
|
||||
*/
|
||||
override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
|
||||
mobileConnectionsRepo.defaultDataSubRatConfig
|
||||
.map { mobileMappingsProxy.mapIconSets(it) }
|
||||
.mapLatest { mobileMappingsProxy.mapIconSets(it) }
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
|
||||
|
||||
/** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
|
||||
override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
|
||||
mobileConnectionsRepo.defaultDataSubRatConfig
|
||||
.map { mobileMappingsProxy.getDefaultIcons(it) }
|
||||
.mapLatest { mobileMappingsProxy.getDefaultIcons(it) }
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
|
||||
|
||||
override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
|
||||
/**
|
||||
* We want to show an error state when cellular has actually failed to validate, but not if some
|
||||
* other transport type is active, because then we expect there not to be validation.
|
||||
*/
|
||||
override val isDefaultConnectionFailed: StateFlow<Boolean> =
|
||||
mobileConnectionsRepo.defaultMobileNetworkConnectivity
|
||||
.mapLatest { connectivityModel ->
|
||||
if (!connectivityModel.isConnected) {
|
||||
false
|
||||
} else {
|
||||
!connectivityModel.isValidated
|
||||
}
|
||||
}
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow
|
||||
|
||||
/** Vends out new [MobileIconInteractor] for a particular subId */
|
||||
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
|
||||
MobileIconInteractorImpl(
|
||||
scope,
|
||||
activeDataConnectionHasDataEnabled,
|
||||
defaultMobileIconMapping,
|
||||
defaultMobileIconGroup,
|
||||
isDefaultConnectionFailed,
|
||||
mobileMappingsProxy,
|
||||
mobileConnectionsRepo.getRepoForSubId(subId),
|
||||
)
|
||||
|
||||
@@ -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,29 +41,38 @@ 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?> =
|
||||
combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) {
|
||||
networkTypeIconGroup,
|
||||
isDataEnabled ->
|
||||
if (!isDataEnabled) {
|
||||
val networkTypeIcon: Flow<Icon?> =
|
||||
combine(
|
||||
iconInteractor.networkTypeIconGroup,
|
||||
iconInteractor.isDataConnected,
|
||||
iconInteractor.isDataEnabled,
|
||||
iconInteractor.isDefaultConnectionFailed,
|
||||
) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection ->
|
||||
if (!dataConnected || !dataEnabled || failedConnection) {
|
||||
null
|
||||
} else {
|
||||
val desc =
|
||||
@@ -72,5 +83,5 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
var tint: Flow<Int> = flowOf(Color.CYAN)
|
||||
val tint: Flow<Int> = flowOf(Color.CYAN)
|
||||
}
|
||||
|
||||
@@ -17,16 +17,18 @@
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeMobileConnectionRepository : MobileConnectionRepository {
|
||||
private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
|
||||
override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow
|
||||
override val subscriptionModelFlow = _subscriptionsModelFlow
|
||||
|
||||
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 +36,8 @@ class FakeMobileConnectionRepository : MobileConnectionRepository {
|
||||
fun setDataEnabled(enabled: Boolean) {
|
||||
_dataEnabled.value = enabled
|
||||
}
|
||||
|
||||
fun setIsDefaultDataSubscription(isDefault: Boolean) {
|
||||
_isDefaultDataSubscription.value = isDefault
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
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 com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -26,18 +27,26 @@ 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 _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
|
||||
override val defaultMobileNetworkConnectivity = _mobileConnectivity
|
||||
|
||||
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 +55,18 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository {
|
||||
_defaultDataSubRatConfig.value = config
|
||||
}
|
||||
|
||||
fun setDefaultDataSubId(id: Int) {
|
||||
_defaultDataSubId.value = id
|
||||
}
|
||||
|
||||
fun setMobileConnectivity(model: MobileConnectivityModel) {
|
||||
_mobileConnectivity.value = model
|
||||
}
|
||||
|
||||
suspend fun triggerGlobalMobileDataSettingChangedEvent() {
|
||||
_globalMobileDataSettingChangedEvent.emit(Unit)
|
||||
}
|
||||
|
||||
fun setActiveMobileDataSubscriptionId(subId: Int) {
|
||||
_activeMobileDataSubscriptionId.value = subId
|
||||
}
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/** Defaults to `true` */
|
||||
class FakeUserSetupRepository : UserSetupRepository {
|
||||
private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val isUserSetupFlow: Flow<Boolean> = _isUserSetup
|
||||
override val isUserSetupFlow = _isUserSetup
|
||||
|
||||
fun setUserSetup(setup: Boolean) {
|
||||
_isUserSetup.value = setup
|
||||
|
||||
@@ -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,
|
||||
@@ -290,14 +300,20 @@ class MobileConnectionRepositoryTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataEnabled_isEnabled() =
|
||||
fun dataEnabled_initial_false() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
|
||||
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
|
||||
assertThat(underTest.dataEnabled.value).isFalse()
|
||||
}
|
||||
|
||||
assertThat(latest).isTrue()
|
||||
@Test
|
||||
fun dataEnabled_isEnabled_true() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
|
||||
val job = underTest.dataEnabled.launchIn(this)
|
||||
|
||||
assertThat(underTest.dataEnabled.value).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
@@ -306,10 +322,59 @@ class MobileConnectionRepositoryTest : SysuiTestCase() {
|
||||
fun dataEnabled_isDisabled() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
|
||||
val job = underTest.dataEnabled.launchIn(this)
|
||||
|
||||
assertThat(underTest.dataEnabled.value).isFalse()
|
||||
|
||||
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()
|
||||
|
||||
@@ -16,26 +16,33 @@
|
||||
|
||||
package com.android.systemui.statusbar.pipeline.mobile.data.repository
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
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.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.mobile.data.model.MobileConnectivityModel
|
||||
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 +50,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
|
||||
@@ -54,32 +60,26 @@ import org.mockito.MockitoAnnotations
|
||||
class MobileConnectionsRepositoryTest : SysuiTestCase() {
|
||||
private lateinit var underTest: MobileConnectionsRepositoryImpl
|
||||
|
||||
@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 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(
|
||||
connectivityManager,
|
||||
subscriptionManager,
|
||||
telephonyManager,
|
||||
logger,
|
||||
broadcastDispatcher,
|
||||
fakeBroadcastDispatcher,
|
||||
globalSettings,
|
||||
context,
|
||||
IMMEDIATE,
|
||||
scope,
|
||||
@@ -214,6 +214,139 @@ 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 mobileConnectivity_default() {
|
||||
assertThat(underTest.defaultMobileNetworkConnectivity.value)
|
||||
.isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mobileConnectivity_isConnected_isValidated() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val caps = createCapabilities(connected = true, validated = true)
|
||||
|
||||
var latest: MobileConnectivityModel? = null
|
||||
val job =
|
||||
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
|
||||
|
||||
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
|
||||
|
||||
assertThat(latest)
|
||||
.isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true))
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mobileConnectivity_isConnected_isNotValidated() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val caps = createCapabilities(connected = true, validated = false)
|
||||
|
||||
var latest: MobileConnectivityModel? = null
|
||||
val job =
|
||||
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
|
||||
|
||||
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
|
||||
|
||||
assertThat(latest)
|
||||
.isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false))
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mobileConnectivity_isNotConnected_isNotValidated() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val caps = createCapabilities(connected = false, validated = false)
|
||||
|
||||
var latest: MobileConnectivityModel? = null
|
||||
val job =
|
||||
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
|
||||
|
||||
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
|
||||
|
||||
assertThat(latest)
|
||||
.isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
/** In practice, I don't think this state can ever happen (!connected, validated) */
|
||||
@Test
|
||||
fun mobileConnectivity_isNotConnected_isValidated() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val caps = createCapabilities(connected = false, validated = true)
|
||||
|
||||
var latest: MobileConnectivityModel? = null
|
||||
val job =
|
||||
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
|
||||
|
||||
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
|
||||
|
||||
assertThat(latest).isEqualTo(MobileConnectivityModel(false, true))
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities =
|
||||
mock<NetworkCapabilities>().also {
|
||||
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected)
|
||||
whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated)
|
||||
}
|
||||
|
||||
private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
|
||||
val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
|
||||
verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
|
||||
return callbackCaptor.value!!
|
||||
}
|
||||
|
||||
private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
|
||||
val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
|
||||
verify(subscriptionManager)
|
||||
@@ -242,5 +375,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
|
||||
private const val SUB_2_ID = 2
|
||||
private val SUB_2 =
|
||||
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
|
||||
|
||||
private const val NET_ID = 123
|
||||
private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,18 +28,23 @@ class FakeMobileIconInteractor : MobileIconInteractor {
|
||||
private val _isEmergencyOnly = MutableStateFlow(false)
|
||||
override val isEmergencyOnly = _isEmergencyOnly
|
||||
|
||||
private val _isFailedConnection = MutableStateFlow(false)
|
||||
override val isDefaultConnectionFailed = _isFailedConnection
|
||||
|
||||
override val isDataConnected = MutableStateFlow(true)
|
||||
|
||||
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 +57,14 @@ class FakeMobileIconInteractor : MobileIconInteractor {
|
||||
_isDataEnabled.value = enabled
|
||||
}
|
||||
|
||||
fun setIsDefaultDataEnabled(disabled: Boolean) {
|
||||
_isDefaultDataEnabled.value = disabled
|
||||
}
|
||||
|
||||
fun setIsFailedConnection(failed: Boolean) {
|
||||
_isFailedConnection.value = failed
|
||||
}
|
||||
|
||||
fun setLevel(level: Int) {
|
||||
_level.value = level
|
||||
}
|
||||
@@ -59,8 +72,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)
|
||||
@@ -46,9 +45,14 @@ class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy)
|
||||
FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
|
||||
)
|
||||
|
||||
override val isDefaultConnectionFailed = MutableStateFlow(false)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest
|
||||
import com.android.settingslib.SignalIcon.MobileIconGroup
|
||||
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.DefaultNetworkType
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
|
||||
@@ -34,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsPro
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -49,12 +51,17 @@ class MobileIconInteractorTest : SysuiTestCase() {
|
||||
private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
|
||||
private val connectionRepository = FakeMobileConnectionRepository()
|
||||
|
||||
private val scope = CoroutineScope(IMMEDIATE)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
underTest =
|
||||
MobileIconInteractorImpl(
|
||||
scope,
|
||||
mobileIconsInteractor.activeDataConnectionHasDataEnabled,
|
||||
mobileIconsInteractor.defaultMobileIconMapping,
|
||||
mobileIconsInteractor.defaultMobileIconGroup,
|
||||
mobileIconsInteractor.isDefaultConnectionFailed,
|
||||
mobileMappingsProxy,
|
||||
connectionRepository,
|
||||
)
|
||||
@@ -196,6 +203,66 @@ 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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_isDefaultConnectionFailed_matchedParent() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val job = underTest.isDefaultConnectionFailed.launchIn(this)
|
||||
|
||||
mobileIconsInteractor.isDefaultConnectionFailed.value = false
|
||||
assertThat(underTest.isDefaultConnectionFailed.value).isFalse()
|
||||
|
||||
mobileIconsInteractor.isDefaultConnectionFailed.value = true
|
||||
assertThat(underTest.isDefaultConnectionFailed.value).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataState_connected() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
|
||||
|
||||
connectionRepository.setMobileSubscriptionModel(
|
||||
MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected)
|
||||
)
|
||||
yield()
|
||||
|
||||
assertThat(latest).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataState_notConnected() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
|
||||
|
||||
connectionRepository.setMobileSubscriptionModel(
|
||||
MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected)
|
||||
)
|
||||
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
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.model.MobileConnectivityModel
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
|
||||
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
|
||||
@@ -32,6 +34,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 +171,92 @@ 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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedConnection_connected_validated_notFailed() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
|
||||
connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true))
|
||||
yield()
|
||||
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedConnection_notConnected_notValidated_notFailed() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
|
||||
|
||||
connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false))
|
||||
yield()
|
||||
|
||||
assertThat(latest).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedConnection_connected_notValidated_failed() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
var latest: Boolean? = null
|
||||
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
|
||||
|
||||
connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false))
|
||||
yield()
|
||||
|
||||
assertThat(latest).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
|
||||
|
||||
@@ -46,10 +46,12 @@ class MobileIconViewModelTest : SysuiTestCase() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
interactor.apply {
|
||||
setLevel(1)
|
||||
setCutOut(false)
|
||||
setIsDefaultDataEnabled(true)
|
||||
setIsFailedConnection(false)
|
||||
setIconGroup(THREE_G)
|
||||
setIsEmergencyOnly(false)
|
||||
setNumberOfLevels(4)
|
||||
isDataConnected.value = true
|
||||
}
|
||||
underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
|
||||
}
|
||||
@@ -59,8 +61,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()
|
||||
}
|
||||
@@ -96,6 +113,44 @@ class MobileIconViewModelTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun networkType_nullWhenFailedConnection() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
interactor.setIconGroup(THREE_G)
|
||||
interactor.setIsDataEnabled(true)
|
||||
interactor.setIsFailedConnection(true)
|
||||
var latest: Icon? = null
|
||||
val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
|
||||
|
||||
assertThat(latest).isNull()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun networkType_nullWhenDataDisconnects() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val initial =
|
||||
Icon.Resource(
|
||||
THREE_G.dataType,
|
||||
ContentDescription.Resource(THREE_G.dataContentDescription)
|
||||
)
|
||||
|
||||
interactor.setIconGroup(THREE_G)
|
||||
var latest: Icon? = null
|
||||
val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
|
||||
|
||||
interactor.setIconGroup(THREE_G)
|
||||
assertThat(latest).isEqualTo(initial)
|
||||
|
||||
interactor.isDataConnected.value = false
|
||||
yield()
|
||||
|
||||
assertThat(latest).isNull()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun networkType_null_changeToDisabled() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
@@ -119,6 +174,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