summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Evan Laird <evanlaird@google.com> 2022-11-01 23:08:59 +0000
committer Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> 2022-11-01 23:08:59 +0000
commitdcd71f012b4eaa7c5c26e5649f45d379a63c4251 (patch)
tree9608d22eab80dc3427f0e45e86d475cfd1043472
parent37e1102c4678f8e663f954ce14e250fa68840d73 (diff)
parent032114691453dd390f80ad95a365f3e733635c20 (diff)
Merge changes I37ab7767,I13890cdc,Idd5fa020,I6f614d61 into tm-qpr-dev am: b91ae18c40 am: 0321146914
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20304061 Change-Id: If264a177efe93b95442b9356d89231702962663b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt108
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt31
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt73
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt166
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt67
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt89
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt67
17 files changed, 830 insertions, 114 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
new file mode 100644
index 000000000000..e61890523ebb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
@@ -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,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 06e8f467ee0b..581842bc2f57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -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,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index 0e2428ae393a..c3c1f1403c60 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -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>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
index 77de849691db..91886bb121d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
@@ -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")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index f99d278c3903..0da84f0bec9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -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)
+ 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
}
- 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
}
- }
+ .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)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 614d583c3c48..a4175c3a6ab1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -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),
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 81317398f086..7869021c0501 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -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)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index de1fec85360b..288f54c7d03c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -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
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 813e750684a0..533d5d9d5b4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -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
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
index 6c495c5c705a..141b50c017e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
@@ -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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
index 093936444789..5ce51bb62c78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -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()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
index 326e0d28166f..a953a3d802e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
@@ -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) }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 5611c448c550..3ae7d3ca1c19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -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
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 2bd228603cb0..061c3b54650e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index ff44af4c9204..7fc1c0f6272c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 877ce0e6b351..b56dcd752557 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index ce0f33f400ab..d4c2c3f6cc2b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -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()
}
@@ -97,6 +114,44 @@ class MobileIconViewModelTest : SysuiTestCase() {
}
@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) {
val expected =
@@ -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