diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt index 73bf188857c97..68cbbceb056d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data -import android.net.Network -import android.net.NetworkCapabilities import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.TelephonyDisplayInfo @@ -27,7 +25,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.dagger.MobileInputLog -import com.android.systemui.statusbar.pipeline.shared.LoggerHelper import javax.inject.Inject /** Logs for inputs into the mobile pipeline. */ @@ -37,24 +34,6 @@ class MobileInputLogger constructor( @MobileInputLog private val buffer: LogBuffer, ) { - fun logOnCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities, - isDefaultNetworkCallback: Boolean, - ) { - LoggerHelper.logOnCapabilitiesChanged( - buffer, - TAG, - network, - networkCapabilities, - isDefaultNetworkCallback, - ) - } - - fun logOnLost(network: Network, isDefaultNetworkCallback: Boolean) { - LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback) - } - fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { buffer.log( TAG, 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 deleted file mode 100644 index 97a537ac0ce67..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 -import com.android.systemui.log.table.Diffable -import com.android.systemui.log.table.TableRowLogger - -/** 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, -) : Diffable { - // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes? - override fun logDiffs(prevVal: MobileConnectivityModel, row: TableRowLogger) { - if (prevVal.isConnected != isConnected) { - row.logChange(COL_IS_CONNECTED, isConnected) - } - if (prevVal.isValidated != isValidated) { - row.logChange(COL_IS_VALIDATED, isValidated) - } - } - - override fun logFull(row: TableRowLogger) { - row.logChange(COL_IS_CONNECTED, isConnected) - row.logChange(COL_IS_VALIDATED, isValidated) - } - - companion object { - private const val COL_IS_CONNECTED = "isConnected" - private const val COL_IS_VALIDATED = "isValidated" - } -} 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 be30ea422bb62..fa712872eb134 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 @@ -21,7 +21,6 @@ import android.telephony.SubscriptionManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -52,8 +51,17 @@ interface MobileConnectionsRepository { /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ val defaultDataSubId: StateFlow - /** The current connectivity status for the default mobile network connection */ - val defaultMobileNetworkConnectivity: StateFlow + /** + * True if the default network connection is a mobile-like connection and false otherwise. + * + * This is typically shown by having [android.net.NetworkCapabilities.TRANSPORT_CELLULAR], but + * there are edge cases (like carrier merged wifi) that could also result in the default + * connection being mobile-like. + */ + val mobileIsDefault: StateFlow + + /** True if the default network connection is validated and false otherwise. */ + val defaultConnectionIsValidated: StateFlow /** Get or create a repository for the line of service for the given subscription ID */ fun getRepoForSubId(subId: Int): MobileConnectionRepository diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index d54531a8370f2..44b5b3fa25911 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -24,7 +24,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl @@ -155,13 +154,18 @@ constructor( .flatMapLatest { it.defaultDataSubId } .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value) - override val defaultMobileNetworkConnectivity: StateFlow = + override val mobileIsDefault: StateFlow = activeRepo - .flatMapLatest { it.defaultMobileNetworkConnectivity } + .flatMapLatest { it.mobileIsDefault } + .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.mobileIsDefault.value) + + override val defaultConnectionIsValidated: StateFlow = + activeRepo + .flatMapLatest { it.defaultConnectionIsValidated } .stateIn( scope, SharingStarted.WhileSubscribed(), - realRepository.defaultMobileNetworkConnectivity.value + realRepository.defaultConnectionIsValidated.value ) override fun getRepoForSubId(subId: Int): MobileConnectionRepository { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 3cafb73772601..737bc6826d08b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -24,7 +24,6 @@ import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBufferFactory -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel @@ -158,8 +157,10 @@ constructor( override val defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) // TODO(b/261029387): not yet supported - override val defaultMobileNetworkConnectivity = - MutableStateFlow(MobileConnectivityModel(isConnected = true, isValidated = true)) + override val mobileIsDefault: StateFlow = MutableStateFlow(true) + + // TODO(b/261029387): not yet supported + override val defaultConnectionIsValidated: StateFlow = MutableStateFlow(true) override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { val current = connectionRepoCache[subId]?.repo diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 991b7868439a1..8c93bf7c21985 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -19,12 +19,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.annotation.SuppressLint import android.content.Context import android.content.IntentFilter -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.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager @@ -46,11 +40,11 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.util.kotlin.pairwise @@ -80,7 +74,7 @@ import kotlinx.coroutines.withContext class MobileConnectionsRepositoryImpl @Inject constructor( - private val connectivityManager: ConnectivityManager, + connectivityRepository: ConnectivityRepository, private val subscriptionManager: SubscriptionManager, private val telephonyManager: TelephonyManager, private val logger: MobileInputLogger, @@ -261,47 +255,31 @@ constructor( subIdRepositoryCache[subId] ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } - @SuppressLint("MissingPermission") - override val defaultMobileNetworkConnectivity: StateFlow = - conflatedCallbackFlow { - val callback = - object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onLost(network: Network) { - logger.logOnLost(network, isDefaultNetworkCallback = true) - // Send a disconnected model when lost. Maybe should create a sealed - // type or null here? - trySend(MobileConnectivityModel()) - } - - override fun onCapabilitiesChanged( - network: Network, - caps: NetworkCapabilities - ) { - logger.logOnCapabilitiesChanged( - network, - caps, - isDefaultNetworkCallback = true, - ) - trySend( - MobileConnectivityModel( - isConnected = caps.hasTransport(TRANSPORT_CELLULAR), - isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED), - ) - ) - } - } - - connectivityManager.registerDefaultNetworkCallback(callback) - - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } + override val mobileIsDefault: StateFlow = + connectivityRepository.defaultConnections + // Because carrier merged networks are displayed as mobile networks, they're + // part of the `isDefault` calculation. See b/272586234. + .map { it.mobile.isDefault || it.carrierMerged.isDefault } .distinctUntilChanged() .logDiffsForTable( tableLogger, - columnPrefix = "$LOGGING_PREFIX.defaultConnection", - initialValue = MobileConnectivityModel(), + columnPrefix = "", + columnName = "mobileIsDefault", + initialValue = false, ) - .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val defaultConnectionIsValidated: StateFlow = + connectivityRepository.defaultConnections + .map { it.isValidated } + .distinctUntilChanged() + .logDiffsForTable( + tableLogger, + columnPrefix = "", + columnName = "defaultConnectionIsValidated", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) /** * Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data 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 7df6764fda1a2..22351f8b28215 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 @@ -22,7 +22,6 @@ import com.android.settingslib.mobile.TelephonyIcons.NOT_DEFAULT_DATA import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository @@ -46,17 +45,8 @@ interface MobileIconInteractor { /** The current mobile data activity */ val activity: Flow - /** - * This bit is meant to be `true` if and only if the default network capabilities (see - * [android.net.ConnectivityManager.registerDefaultNetworkCallback]) result in a network that - * has the [android.net.NetworkCapabilities.TRANSPORT_CELLULAR] represented. - * - * Note that this differs from [isDataConnected], which is tracked by telephony and has to do - * with the state of using this mobile connection for data as opposed to just voice. It is - * possible for a mobile subscription to be connected but not be in a connected data state, and - * thus we wouldn't want to show the network type icon. - */ - val isConnected: Flow + /** See [MobileConnectionsRepository.mobileIsDefault]. */ + val mobileIsDefault: Flow /** * True when telephony tells us that the data state is CONNECTED. See @@ -126,7 +116,7 @@ class MobileIconInteractorImpl( defaultSubscriptionHasDataEnabled: StateFlow, override val alwaysShowDataRatIcon: StateFlow, override val alwaysUseCdmaLevel: StateFlow, - defaultMobileConnectivity: StateFlow, + override val mobileIsDefault: StateFlow, defaultMobileIconMapping: StateFlow>, defaultMobileIconGroup: StateFlow, defaultDataSubId: StateFlow, @@ -138,8 +128,6 @@ class MobileIconInteractorImpl( override val activity = connectionRepository.dataActivityDirection - override val isConnected: Flow = defaultMobileConnectivity.mapLatest { it.isConnected } - override val isDataEnabled: StateFlow = connectionRepository.dataEnabled private val isDefault = 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 142c372d4c07e..6c8310ac3d294 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 @@ -25,7 +25,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository @@ -61,8 +60,12 @@ import kotlinx.coroutines.flow.transformLatest * icon */ interface MobileIconsInteractor { + /** See [MobileConnectionsRepository.mobileIsDefault]. */ + val mobileIsDefault: StateFlow + /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow> + /** True if the active mobile data subscription has data enabled */ val activeDataConnectionHasDataEnabled: StateFlow @@ -75,20 +78,15 @@ interface MobileIconsInteractor { /** Tracks the subscriptionId set as the default for data connections */ val defaultDataSubId: StateFlow - /** - * The connectivity of the default mobile network. Note that this can differ from what is - * reported from [MobileConnectionsRepository] in some cases. E.g., when the active subscription - * changes but the groupUuid remains the same, we keep the old validation information for 2 - * seconds to avoid icon flickering. - */ - val defaultMobileNetworkConnectivity: StateFlow - /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: StateFlow> + /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ val defaultMobileIconGroup: StateFlow + /** True only if the default network is mobile, and validation also failed */ val isDefaultConnectionFailed: StateFlow + /** True once the user has been set up */ val isUserSetup: StateFlow @@ -115,6 +113,9 @@ constructor( userSetupRepo: UserSetupRepository, @Application private val scope: CoroutineScope, ) : MobileIconsInteractor { + + override val mobileIsDefault = mobileConnectionsRepo.mobileIsDefault + override val activeDataConnectionHasDataEnabled: StateFlow = mobileConnectionsRepo.activeMobileDataRepository .flatMapLatest { it?.dataEnabled ?: flowOf(false) } @@ -197,7 +198,7 @@ constructor( */ private val forcingCellularValidation = mobileConnectionsRepo.activeSubChangedInGroupEvent - .filter { mobileConnectionsRepo.defaultMobileNetworkConnectivity.value.isValidated } + .filter { mobileConnectionsRepo.defaultConnectionIsValidated.value } .transformLatest { emit(true) delay(2000) @@ -211,32 +212,6 @@ constructor( ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) - override val defaultMobileNetworkConnectivity: StateFlow = - combine( - mobileConnectionsRepo.defaultMobileNetworkConnectivity, - forcingCellularValidation, - ) { networkConnectivity, forceValidation -> - return@combine if (forceValidation) { - MobileConnectivityModel( - isValidated = true, - isConnected = networkConnectivity.isConnected - ) - } else { - networkConnectivity - } - } - .distinctUntilChanged() - .logDiffsForTable( - tableLogger, - columnPrefix = "$LOGGING_PREFIX.defaultConnection", - initialValue = mobileConnectionsRepo.defaultMobileNetworkConnectivity.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - mobileConnectionsRepo.defaultMobileNetworkConnectivity.value - ) - /** * Mapping from network type to [MobileIconGroup] using the config generated for the default * subscription Id. This mapping is the same for every subscription. @@ -271,12 +246,15 @@ constructor( * other transport type is active, because then we expect there not to be validation. */ override val isDefaultConnectionFailed: StateFlow = - mobileConnectionsRepo.defaultMobileNetworkConnectivity - .mapLatest { connectivityModel -> - if (!connectivityModel.isConnected) { - false - } else { - !connectivityModel.isValidated + combine( + mobileConnectionsRepo.mobileIsDefault, + mobileConnectionsRepo.defaultConnectionIsValidated, + forcingCellularValidation, + ) { mobileIsDefault, defaultConnectionIsValidated, forcingCellularValidation -> + when { + !mobileIsDefault -> false + forcingCellularValidation -> false + else -> !defaultConnectionIsValidated } } .logDiffsForTable( @@ -301,7 +279,7 @@ constructor( activeDataConnectionHasDataEnabled, alwaysShowDataRatIcon, alwaysUseCdmaLevel, - defaultMobileNetworkConnectivity, + mobileIsDefault, defaultMobileIconMapping, defaultMobileIconGroup, defaultDataSubId, 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 dbb534b244716..0fd007cf40ef5 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 @@ -148,9 +148,9 @@ constructor( iconInteractor.isDataEnabled, iconInteractor.isDefaultConnectionFailed, iconInteractor.alwaysShowDataRatIcon, - iconInteractor.isConnected, - ) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected -> - alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected) + iconInteractor.mobileIsDefault, + ) { dataConnected, dataEnabled, failedConnection, alwaysShow, mobileIsDefault -> + alwaysShow || (dataConnected && dataEnabled && !failedConnection && mobileIsDefault) } .distinctUntilChanged() .logDiffsForTable( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt index 95548b84f7693..82492babba46d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt @@ -16,10 +16,13 @@ package com.android.systemui.statusbar.pipeline.shared +import android.net.Network +import android.net.NetworkCapabilities import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.dagger.SharedConnectivityInputLog +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel import javax.inject.Inject /** Logs for connectivity-related inputs that are shared across wifi, mobile, etc. */ @@ -32,6 +35,32 @@ constructor( fun logTuningChanged(tuningList: String?) { buffer.log(TAG, LogLevel.DEBUG, { str1 = tuningList }, { "onTuningChanged: $str1" }) } + + fun logOnDefaultCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities, + ) { + LoggerHelper.logOnCapabilitiesChanged( + buffer, + TAG, + network, + networkCapabilities, + isDefaultNetworkCallback = true, + ) + } + + fun logOnDefaultLost(network: Network) { + LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback = true) + } + + fun logDefaultConnectionsChanged(model: DefaultConnectionModel) { + buffer.log( + TAG, + LogLevel.DEBUG, + model::messageInitializer, + model::messagePrinter, + ) + } } private const val TAG = "ConnectivityInputLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt new file mode 100644 index 0000000000000..2a02687f0761f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 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.shared.data.model + +import android.net.NetworkCapabilities +import com.android.systemui.plugins.log.LogMessage + +/** + * A model for all of the current default connections(s). + * + * Uses different classes for each connection type to ensure type safety when setting the values. + * + * Important: We generally expect there to be only *one* default network at a time (with the + * exception of carrier merged). Specifically, we don't expect to ever have both wifi *and* cellular + * as default at the same time. However, the framework network callbacks don't provide any + * guarantees about why types of network could be default at the same time, so we don't enforce any + * guarantees on this class. + */ +data class DefaultConnectionModel( + /** Wifi's status as default or not. */ + val wifi: Wifi = Wifi(isDefault = false), + + /** Mobile's status as default or not. */ + val mobile: Mobile = Mobile(isDefault = false), + + /** + * True if the current default network represents a carrier merged network, and false otherwise. + * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. + * + * Important: A carrier merged network can come in as either a + * [NetworkCapabilities.TRANSPORT_CELLULAR] *or* as a [NetworkCapabilities.TRANSPORT_WIFI]. This + * means that when carrier merged is in effect, either: + * - [wifi] *and* [carrierMerged] will be marked as default; or + * - [mobile] *and* [carrierMerged] will be marked as default + * + * Specifically, [carrierMerged] will never be the *only* default connection. + */ + val carrierMerged: CarrierMerged = CarrierMerged(isDefault = false), + + /** Ethernet's status as default or not. */ + val ethernet: Ethernet = Ethernet(isDefault = false), + + /** True if the default connection is currently validated and false otherwise. */ + val isValidated: Boolean = false, +) { + data class Wifi(val isDefault: Boolean) + data class Mobile(val isDefault: Boolean) + data class CarrierMerged(val isDefault: Boolean) + data class Ethernet(val isDefault: Boolean) + + /** + * Used in conjunction with [ConnectivityInputLogger] to log this class without calling + * [toString] on it. + * + * Be sure to change [messagePrinter] whenever this method is changed. + */ + fun messageInitializer(message: LogMessage) { + message.bool1 = wifi.isDefault + message.bool2 = mobile.isDefault + message.bool3 = carrierMerged.isDefault + message.bool4 = ethernet.isDefault + message.int1 = if (isValidated) 1 else 0 + } + + fun messagePrinter(message: LogMessage): String { + return "DefaultConnectionModel(" + + "wifi.isDefault=${message.bool1}, " + + "mobile.isDefault=${message.bool2}, " + + "carrierMerged.isDefault=${message.bool3}, " + + "ethernet.isDefault=${message.bool4}, " + + "isValidated=${if (message.int1 == 1) "true" else "false"})" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt index 5d9ba018822d6..6479f3d9f8a6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt @@ -16,7 +16,17 @@ package com.android.systemui.statusbar.pipeline.shared.data.repository +import android.annotation.SuppressLint import android.content.Context +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.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.vcn.VcnTransportInfo +import android.net.wifi.WifiInfo import androidx.annotation.ArrayRes import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable @@ -29,6 +39,11 @@ import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.pipeline.shared.ConnectivityInputLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.CarrierMerged +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.Ethernet +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.Mobile +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.Wifi import com.android.systemui.tuner.TunerService import java.io.PrintWriter import javax.inject.Inject @@ -37,6 +52,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** @@ -46,6 +63,9 @@ import kotlinx.coroutines.flow.stateIn interface ConnectivityRepository { /** Observable for the current set of connectivity icons that should be force-hidden. */ val forceHiddenSlots: StateFlow> + + /** Observable for which connection(s) are currently default. */ + val defaultConnections: StateFlow } @OptIn(ExperimentalCoroutinesApi::class) @@ -53,6 +73,7 @@ interface ConnectivityRepository { class ConnectivityRepositoryImpl @Inject constructor( + connectivityManager: ConnectivityManager, private val connectivitySlots: ConnectivitySlots, context: Context, dumpManager: DumpManager, @@ -61,7 +82,7 @@ constructor( tunerService: TunerService, ) : ConnectivityRepository, Dumpable { init { - dumpManager.registerDumpable("ConnectivityRepository", this) + dumpManager.registerNormalDumpable("ConnectivityRepository", this) } // The default set of hidden icons to use if we don't get any from [TunerService]. @@ -97,6 +118,67 @@ constructor( initialValue = defaultHiddenIcons ) + @SuppressLint("MissingPermission") + override val defaultConnections: StateFlow = + conflatedCallbackFlow { + val callback = + object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onLost(network: Network) { + logger.logOnDefaultLost(network) + // The system no longer has a default network, so everything is + // non-default. + trySend( + DefaultConnectionModel( + Wifi(isDefault = false), + Mobile(isDefault = false), + CarrierMerged(isDefault = false), + Ethernet(isDefault = false), + isValidated = false, + ) + ) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities, + ) { + logger.logOnDefaultCapabilitiesChanged(network, networkCapabilities) + + val isWifiDefault = + networkCapabilities.hasTransport(TRANSPORT_WIFI) || + networkCapabilities.getMainOrUnderlyingWifiInfo() != null + val isMobileDefault = + networkCapabilities.hasTransport(TRANSPORT_CELLULAR) + val isCarrierMergedDefault = + networkCapabilities + .getMainOrUnderlyingWifiInfo() + ?.isCarrierMerged == true + val isEthernetDefault = + networkCapabilities.hasTransport(TRANSPORT_ETHERNET) + + val isValidated = + networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) + + trySend( + DefaultConnectionModel( + Wifi(isWifiDefault), + Mobile(isMobileDefault), + CarrierMerged(isCarrierMergedDefault), + Ethernet(isEthernetDefault), + isValidated, + ) + ) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .distinctUntilChanged() + .onEach { logger.logDefaultConnectionsChanged(it) } + .stateIn(scope, SharingStarted.Eagerly, DefaultConnectionModel()) + override fun dump(pw: PrintWriter, args: Array) { pw.apply { println("defaultHiddenIcons=$defaultHiddenIcons") } } @@ -116,5 +198,35 @@ constructor( .mapNotNull { connectivitySlots.getSlotFromName(it) } .toSet() } + + /** + * Returns a [WifiInfo] object from the capabilities if it has one, or null if there is no + * underlying wifi network. + * + * This will return a valid [WifiInfo] object if wifi is the main transport **or** wifi is + * an underlying transport. This is important for carrier merged networks, where the main + * transport info is *not* wifi, but the underlying transport info *is* wifi. We want to + * always use [WifiInfo] if it's available, so we need to check the underlying transport + * info. + */ + fun NetworkCapabilities.getMainOrUnderlyingWifiInfo(): WifiInfo? { + // Wifi info can either come from a WIFI Transport, or from a CELLULAR transport for + // virtual networks like VCN. + val canHaveWifiInfo = + this.hasTransport(TRANSPORT_CELLULAR) || this.hasTransport(TRANSPORT_WIFI) + if (!canHaveWifiInfo) { + return null + } + + return when (val currentTransportInfo = transportInfo) { + // This VcnTransportInfo logic is copied from + // [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of + // re-used because it makes the logic here clearer, and because the method will be + // removed once this pipeline is fully launched. + is VcnTransportInfo -> currentTransportInfo.wifiInfo + is WifiInfo -> currentTransportInfo + else -> null + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index b5e7b7a135054..f80aa688268fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -30,7 +30,6 @@ import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID -import com.android.settingslib.Utils import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -41,6 +40,8 @@ import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.getMainOrUnderlyingWifiInfo import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger @@ -55,6 +56,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -70,6 +72,7 @@ class WifiRepositoryImpl constructor( broadcastDispatcher: BroadcastDispatcher, connectivityManager: ConnectivityManager, + connectivityRepository: ConnectivityRepository, logger: WifiInputLogger, @WifiTableLog wifiTableLogBuffer: TableLogBuffer, @Main mainExecutor: Executor, @@ -105,39 +108,9 @@ constructor( ) override val isWifiDefault: StateFlow = - conflatedCallbackFlow { - // Note: This callback doesn't do any logging because we already log every network - // change in the [wifiNetwork] callback. - val callback = - object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - logger.logOnCapabilitiesChanged( - network, - networkCapabilities, - isDefaultNetworkCallback = true, - ) - - // This method will always be called immediately after the network - // becomes the default, in addition to any time the capabilities change - // while the network is the default. - // If this network is a wifi network, then wifi is the default network. - trySend(isWifiNetwork(networkCapabilities)) - } - - override fun onLost(network: Network) { - logger.logOnLost(network, isDefaultNetworkCallback = true) - // The system no longer has a default network, so wifi is definitely not - // default. - trySend(false) - } - } - - connectivityManager.registerDefaultNetworkCallback(callback) - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } + connectivityRepository.defaultConnections + // TODO(b/274493701): Should wifi be considered default if it's carrier merged? + .map { it.wifi.isDefault || it.carrierMerged.isDefault } .distinctUntilChanged() .logDiffsForTable( wifiTableLogBuffer, @@ -165,7 +138,7 @@ constructor( wifiNetworkChangeEvents.tryEmit(Unit) - val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + val wifiInfo = networkCapabilities.getMainOrUnderlyingWifiInfo() if (wifiInfo?.isPrimary == true) { val wifiNetworkModel = createWifiNetworkModel( @@ -248,34 +221,6 @@ constructor( // NetworkCallback inside [wifiNetwork] for our wifi network information. val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive - private fun networkCapabilitiesToWifiInfo( - networkCapabilities: NetworkCapabilities - ): WifiInfo? { - return when { - networkCapabilities.hasTransport(TRANSPORT_CELLULAR) -> - // Sometimes, cellular networks can act as wifi networks (known as VCN -- - // virtual carrier network). So, see if this cellular network has wifi info. - Utils.tryGetWifiInfoForVcn(networkCapabilities) - networkCapabilities.hasTransport(TRANSPORT_WIFI) -> - if (networkCapabilities.transportInfo is WifiInfo) { - networkCapabilities.transportInfo as WifiInfo - } else { - null - } - else -> null - } - } - - /** True if these capabilities represent a wifi network. */ - private fun isWifiNetwork(networkCapabilities: NetworkCapabilities): Boolean { - return when { - networkCapabilities.hasTransport(TRANSPORT_WIFI) -> true - networkCapabilities.hasTransport(TRANSPORT_CELLULAR) -> - Utils.tryGetWifiInfoForVcn(networkCapabilities) != null - else -> false - } - } - private fun createWifiNetworkModel( wifiInfo: WifiInfo, network: Network, @@ -337,6 +282,7 @@ constructor( constructor( private val broadcastDispatcher: BroadcastDispatcher, private val connectivityManager: ConnectivityManager, + private val connectivityRepository: ConnectivityRepository, private val logger: WifiInputLogger, @WifiTableLog private val wifiTableLogBuffer: TableLogBuffer, @Main private val mainExecutor: Executor, @@ -346,6 +292,7 @@ constructor( return WifiRepositoryImpl( broadcastDispatcher, connectivityManager, + connectivityRepository, logger, wifiTableLogBuffer, mainExecutor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt deleted file mode 100644 index 7c9351c8495d8..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 - -import android.net.Network -import android.net.NetworkCapabilities -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBufferFactory -import com.android.systemui.plugins.log.LogcatEchoTracker -import com.google.common.truth.Truth.assertThat -import java.io.PrintWriter -import java.io.StringWriter -import org.junit.Test -import org.mockito.Mockito -import org.mockito.Mockito.mock - -@SmallTest -class MobileInputLoggerTest : SysuiTestCase() { - private val buffer = - LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java)).create("buffer", 10) - private val logger = MobileInputLogger(buffer) - - @Test - fun testLogNetworkCapsChange_bufferHasInfo() { - logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS, isDefaultNetworkCallback = true) - - val stringWriter = StringWriter() - buffer.dump(PrintWriter(stringWriter), tailLength = 0) - val actualString = stringWriter.toString() - - val expectedNetId = NET_1_ID.toString() - val expectedCaps = NET_1_CAPS.toString() - - assertThat(actualString).contains("onDefaultCapabilitiesChanged") - assertThat(actualString).contains(expectedNetId) - assertThat(actualString).contains(expectedCaps) - } - - @Test - fun testLogOnLost_bufferHasNetIdOfLostNetwork() { - logger.logOnLost(NET_1, isDefaultNetworkCallback = false) - - val stringWriter = StringWriter() - buffer.dump(PrintWriter(stringWriter), tailLength = 0) - val actualString = stringWriter.toString() - - val expectedNetId = NET_1_ID.toString() - - assertThat(actualString).contains("onLost") - assertThat(actualString).contains(expectedNetId) - } - - companion object { - private const val NET_1_ID = 100 - private val NET_1 = - com.android.systemui.util.mockito.mock().also { - Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID) - } - private val NET_1_CAPS = - NetworkCapabilities.Builder() - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - } -} 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 f483e42056f39..f9c72d5236738 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 @@ -23,7 +23,6 @@ import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableSharedFlow @@ -66,8 +65,9 @@ class FakeMobileConnectionsRepository( private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val defaultDataSubId = _defaultDataSubId - private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel()) - override val defaultMobileNetworkConnectivity = _mobileConnectivity + override val mobileIsDefault = MutableStateFlow(false) + + override val defaultConnectionIsValidated = MutableStateFlow(false) private val subIdRepos = mutableMapOf() @@ -88,14 +88,6 @@ class FakeMobileConnectionsRepository( _subscriptions.value = subs } - fun setDefaultDataSubId(id: Int) { - _defaultDataSubId.value = id - } - - fun setMobileConnectivity(model: MobileConnectivityModel) { - _mobileConnectivity.value = model - } - fun setActiveMobileDataSubscriptionId(subId: Int) { // Simulate the filtering that the repo does if (subId == INVALID_SUBSCRIPTION_ID) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 07c8cee9a3d4f..0b20285323079 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository -import android.net.ConnectivityManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyManager @@ -35,6 +34,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource import com.android.systemui.util.mockito.any @@ -77,8 +78,8 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private lateinit var wifiDataSource: DemoModeWifiDataSource private lateinit var logFactory: TableLogBufferFactory private lateinit var wifiRepository: FakeWifiRepository + private lateinit var connectivityRepository: ConnectivityRepository - @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: MobileInputLogger @@ -110,9 +111,11 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { } wifiRepository = FakeWifiRepository() + connectivityRepository = FakeConnectivityRepository() + realRepo = MobileConnectionsRepositoryImpl( - connectivityManager, + connectivityRepository, subscriptionManager, telephonyManager, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index 0e45d8ea55634..47f8cd319bffe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -91,11 +91,17 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test - fun `connectivity - defaults to connected and validated`() = + fun isDefault_defaultsToTrue() = testScope.runTest { - val connectivity = underTest.defaultMobileNetworkConnectivity.value - assertThat(connectivity.isConnected).isTrue() - assertThat(connectivity.isValidated).isTrue() + val isDefault = underTest.mobileIsDefault.value + assertThat(isDefault).isTrue() + } + + @Test + fun validated_defaultsToTrue() = + testScope.runTest { + val isValidated = underTest.defaultConnectionIsValidated.value + assertThat(isValidated).isTrue() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 68b1cda62f4c3..d65277f37ec48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -22,6 +22,10 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.vcn.VcnTransportInfo +import android.net.wifi.WifiInfo import android.os.ParcelUuid import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo @@ -38,12 +42,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.util.mockito.any @@ -81,6 +87,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory + private lateinit var connectivityRepository: ConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var carrierConfigRepository: CarrierConfigRepository @Mock private lateinit var connectivityManager: ConnectivityManager @@ -121,6 +128,17 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } + connectivityRepository = + ConnectivityRepositoryImpl( + connectivityManager, + ConnectivitySlots(context), + context, + mock(), + mock(), + scope, + mock(), + ) + wifiRepository = FakeWifiRepository() carrierConfigRepository = @@ -159,7 +177,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { underTest = MobileConnectionsRepositoryImpl( - connectivityManager, + connectivityRepository, subscriptionManager, telephonyManager, logger, @@ -668,75 +686,205 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test - fun mobileConnectivity_default() { - assertThat(underTest.defaultMobileNetworkConnectivity.value) - .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + fun mobileIsDefault_startsAsFalse() { + assertThat(underTest.mobileIsDefault.value).isFalse() } @Test - fun mobileConnectivity_isConnected_isValidated() = + fun mobileIsDefault_capsHaveCellular_isDefault() = runBlocking(IMMEDIATE) { - val caps = createCapabilities(connected = true, validated = true) + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + } - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - assertThat(latest) - .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true)) + assertThat(latest).isTrue() job.cancel() } @Test - fun mobileConnectivity_isConnected_isNotValidated() = + fun mobileIsDefault_capsDoNotHaveCellular_isNotDefault() = runBlocking(IMMEDIATE) { - val caps = createCapabilities(connected = true, validated = false) + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + } - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - assertThat(latest) - .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false)) + assertThat(latest).isFalse() + + job.cancel() + } + + /** Regression test for b/272586234. */ + @Test + fun mobileIsDefault_carrierMergedViaWifi_isDefault() = + runBlocking(IMMEDIATE) { + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + } + + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isTrue() job.cancel() } @Test - fun mobileConnectivity_isNotConnected_isNotValidated() = + fun mobileIsDefault_carrierMergedViaMobile_isDefault() = runBlocking(IMMEDIATE) { - val caps = createCapabilities(connected = false, validated = false) + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + } - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - assertThat(latest) - .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + assertThat(latest).isTrue() job.cancel() } - /** In practice, I don't think this state can ever happen (!connected, validated) */ + /** Regression test for b/272586234. */ @Test - fun mobileConnectivity_isNotConnected_isValidated() = + fun mobileIsDefault_carrierMergedViaWifiWithVcnTransport_isDefault() = runBlocking(IMMEDIATE) { - val caps = createCapabilities(connected = false, validated = true) + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + } - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - assertThat(latest).isEqualTo(MobileConnectivityModel(false, true)) + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun mobileIsDefault_carrierMergedViaMobileWithVcnTransport_isDefault() = + runBlocking(IMMEDIATE) { + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + } + + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun mobileIsDefault_wifiDefault_mobileNotDefault() = + runBlocking(IMMEDIATE) { + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + } + + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun mobileIsDefault_ethernetDefault_mobileNotDefault() = + runBlocking(IMMEDIATE) { + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(true) + } + + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun defaultConnectionIsValidated_startsAsFalse() { + assertThat(underTest.defaultConnectionIsValidated.value).isFalse() + } + + @Test + fun defaultConnectionIsValidated_capsHaveValidated_isValidated() = + runBlocking(IMMEDIATE) { + val caps = + mock().also { + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true) + } + + var latest: Boolean? = null + val job = underTest.defaultConnectionIsValidated.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun defaultConnectionIsValidated_capsHaveNotValidated_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = + mock().also { + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(false) + } + + var latest: Boolean? = null + val job = underTest.defaultConnectionIsValidated.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isFalse() job.cancel() } @@ -752,7 +900,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // the resources and then re-create the repo. underTest = MobileConnectionsRepositoryImpl( - connectivityManager, + connectivityRepository, subscriptionManager, telephonyManager, logger, @@ -860,12 +1008,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } - private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities = - mock().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected) - whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated) - } - private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { val callbackCaptor = argumentCaptor() verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) 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 b645e667e1839..8d2c5695c7c4b 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 @@ -40,7 +40,7 @@ class FakeMobileIconInteractor( ) ) - override val isConnected = MutableStateFlow(true) + override val mobileIsDefault = MutableStateFlow(true) private val _iconGroup = MutableStateFlow(TelephonyIcons.THREE_G) override val networkTypeIconGroup = _iconGroup 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 2699316d1b0bf..d6fdad417b319 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 @@ -23,7 +23,6 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow @@ -62,7 +61,7 @@ class FakeMobileIconsInteractor( override val alwaysUseCdmaLevel = MutableStateFlow(false) override val defaultDataSubId = MutableStateFlow(DEFAULT_DATA_SUB_ID) - override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) + override val mobileIsDefault = MutableStateFlow(false) 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 1eb1056204cdf..2054e8b12efff 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 @@ -60,7 +60,7 @@ class MobileIconInteractorTest : SysuiTestCase() { mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.alwaysShowDataRatIcon, mobileIconsInteractor.alwaysUseCdmaLevel, - mobileIconsInteractor.defaultMobileNetworkConnectivity, + mobileIconsInteractor.mobileIsDefault, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, mobileIconsInteractor.defaultDataSubId, 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 c51dbf11e751a..898e897703946 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 @@ -22,7 +22,6 @@ import androidx.test.filters.SmallTest import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -307,11 +306,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test - fun failedConnection_connected_validated_notFailed() = + fun failedConnection_default_validated_notFailed() = testScope.runTest { var latest: Boolean? = null val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) - connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true)) + + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = true yield() assertThat(latest).isFalse() @@ -320,12 +321,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test - fun failedConnection_notConnected_notValidated_notFailed() = + fun failedConnection_notDefault_notValidated_notFailed() = testScope.runTest { var latest: Boolean? = null val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) - connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false)) + connectionsRepository.mobileIsDefault.value = false + connectionsRepository.defaultConnectionIsValidated.value = false yield() assertThat(latest).isFalse() @@ -334,12 +336,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test - fun failedConnection_connected_notValidated_failed() = + fun failedConnection_default_notValidated_failed() = testScope.runTest { var latest: Boolean? = null val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) - connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false)) + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = false yield() assertThat(latest).isTrue() @@ -347,6 +350,46 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + /** Regression test for b/275076959. */ + @Test + fun failedConnection_dataSwitchInSameGroup_notFailed() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = true + + // WHEN there's a data change in the same subscription group + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + connectionsRepository.defaultConnectionIsValidated.value = false + + // THEN the default connection is *not* marked as failed because of forced validation + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_dataSwitchNotInSameGroup_isFailed() = + testScope.runTest { + var latestConnectionFailed: Boolean? = null + val job = + underTest.isDefaultConnectionFailed + .onEach { latestConnectionFailed = it } + .launchIn(this) + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = true + + // WHEN the connection is invalidated without a activeSubChangedInGroupEvent + connectionsRepository.defaultConnectionIsValidated.value = false + + // THEN the connection is immediately marked as failed + assertThat(latestConnectionFailed).isTrue() + + job.cancel() + } + @Test fun alwaysShowDataRatIcon_configHasTrue() = testScope.runTest { @@ -412,137 +455,69 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test - fun `default mobile connectivity - uses repo value`() = + fun mobileIsDefault_usesRepoValue() = testScope.runTest { - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) - var expected = MobileConnectivityModel(isConnected = true, isValidated = true) - connectionsRepository.setMobileConnectivity(expected) - assertThat(latest).isEqualTo(expected) + connectionsRepository.mobileIsDefault.value = true + assertThat(latest).isTrue() - expected = MobileConnectivityModel(isConnected = false, isValidated = true) - connectionsRepository.setMobileConnectivity(expected) - assertThat(latest).isEqualTo(expected) + connectionsRepository.mobileIsDefault.value = false + assertThat(latest).isFalse() - expected = MobileConnectivityModel(isConnected = true, isValidated = false) - connectionsRepository.setMobileConnectivity(expected) - assertThat(latest).isEqualTo(expected) - - expected = MobileConnectivityModel(isConnected = false, isValidated = false) - connectionsRepository.setMobileConnectivity(expected) - assertThat(latest).isEqualTo(expected) + connectionsRepository.mobileIsDefault.value = true + assertThat(latest).isTrue() job.cancel() } - @Test - fun `data switch - in same group - validated matches previous value`() = - testScope.runTest { - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) - - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = true, - isValidated = true, - ) - ) - // Trigger a data change in the same subscription group - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) - - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = true, - ) - ) - - job.cancel() - } + // The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow + // is private and can only be tested by looking at [isDefaultConnectionFailed]. @Test fun `data switch - in same group - validated matches previous value - expires after 2s`() = testScope.runTest { - var latest: MobileConnectivityModel? = null + var latestConnectionFailed: Boolean? = null val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + underTest.isDefaultConnectionFailed + .onEach { latestConnectionFailed = it } + .launchIn(this) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = true, - isValidated = true, - ) - ) - // Trigger a data change in the same subscription group + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = true + + // Trigger a data change in the same subscription group that's not yet validated connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) - // After 1s, the force validation bit is still present + connectionsRepository.defaultConnectionIsValidated.value = false + + // After 1s, the force validation bit is still present, so the connection is not marked + // as failed advanceTimeBy(1000) - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = true, - ) - ) + assertThat(latestConnectionFailed).isFalse() - // After 2s, the force validation expires + // After 2s, the force validation expires so the connection updates to failed advanceTimeBy(1001) - - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) + assertThat(latestConnectionFailed).isTrue() job.cancel() } @Test - fun `data switch - in same group - not validated - uses new value immediately`() = + fun `data switch - in same group - not validated - immediately marked as failed`() = testScope.runTest { - var latest: MobileConnectivityModel? = null + var latestConnectionFailed: Boolean? = null val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + underTest.isDefaultConnectionFailed + .onEach { latestConnectionFailed = it } + .launchIn(this) + + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = false - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = true, - isValidated = false, - ) - ) connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) + assertThat(latestConnectionFailed).isTrue() job.cancel() } @@ -550,60 +525,34 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun `data switch - lose validation - then switch happens - clears forced bit`() = testScope.runTest { - var latest: MobileConnectivityModel? = null + var latestConnectionFailed: Boolean? = null val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + underTest.isDefaultConnectionFailed + .onEach { latestConnectionFailed = it } + .launchIn(this) // GIVEN the network starts validated - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = true, - isValidated = true, - ) - ) + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = true // WHEN a data change happens in the same group connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) // WHEN the validation bit is lost - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) + connectionsRepository.defaultConnectionIsValidated.value = false // WHEN another data change happens in the same group connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - // THEN the forced validation bit is still removed after 2s - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = true, - ) - ) + // THEN the forced validation bit is still used... + assertThat(latestConnectionFailed).isFalse() advanceTimeBy(1000) + assertThat(latestConnectionFailed).isFalse() - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = true, - ) - ) - + // ... but expires after 2s advanceTimeBy(1001) - - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) + assertThat(latestConnectionFailed).isTrue() job.cancel() } @@ -611,15 +560,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun `data switch - while already forcing validation - resets clock`() = testScope.runTest { - var latest: MobileConnectivityModel? = null + var latestConnectionFailed: Boolean? = null val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = true, - isValidated = true, - ) - ) + underTest.isDefaultConnectionFailed + .onEach { latestConnectionFailed = it } + .launchIn(this) + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultConnectionIsValidated.value = true connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) @@ -627,65 +574,17 @@ class MobileIconsInteractorTest : SysuiTestCase() { // WHEN another change in same group event happens connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) + connectionsRepository.defaultConnectionIsValidated.value = false // THEN the forced validation remains for exactly 2 more seconds from now // 1.500s from second event advanceTimeBy(1500) - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = true, - ) - ) + assertThat(latestConnectionFailed).isFalse() // 2.001s from the second event advanceTimeBy(501) - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) - - job.cancel() - } - - @Test - fun `data switch - not in same group - uses new values`() = - testScope.runTest { - var latest: MobileConnectivityModel? = null - val job = - underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) - - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = true, - isValidated = true, - ) - ) - connectionsRepository.setMobileConnectivity( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) - - assertThat(latest) - .isEqualTo( - MobileConnectivityModel( - isConnected = false, - isValidated = false, - ) - ) + assertThat(latestConnectionFailed).isTrue() job.cancel() } 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 bec276a9c68fc..8ea8f87e6affa 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 @@ -400,10 +400,10 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test - fun `network type - alwaysShow - shown when not connected`() = + fun `network type - alwaysShow - shown when not default`() = testScope.runTest { interactor.setIconGroup(THREE_G) - interactor.isConnected.value = false + interactor.mobileIsDefault.value = false interactor.alwaysShowDataRatIcon.value = true var latest: Icon? = null @@ -420,11 +420,11 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test - fun `network type - not shown when not connected`() = + fun `network type - not shown when not default`() = testScope.runTest { interactor.setIconGroup(THREE_G) interactor.isDataConnected.value = true - interactor.isConnected.value = false + interactor.mobileIsDefault.value = false var latest: Icon? = null val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModelTest.kt new file mode 100644 index 0000000000000..03cd94fa9d704 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModelTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 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.shared.data.model + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.log.LogMessageImpl +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class DefaultConnectionModelTest : SysuiTestCase() { + @Test + fun messageInitializerAndPrinter_isValidatedFalse_hasCorrectInfo() { + val model = + DefaultConnectionModel( + DefaultConnectionModel.Wifi(isDefault = false), + DefaultConnectionModel.Mobile(isDefault = true), + DefaultConnectionModel.CarrierMerged(isDefault = true), + DefaultConnectionModel.Ethernet(isDefault = false), + isValidated = false, + ) + val message = LogMessageImpl.create() + + model.messageInitializer(message) + val messageString = model.messagePrinter(message) + + assertThat(messageString).contains("wifi.isDefault=false") + assertThat(messageString).contains("mobile.isDefault=true") + assertThat(messageString).contains("carrierMerged.isDefault=true") + assertThat(messageString).contains("ethernet.isDefault=false") + assertThat(messageString).contains("isValidated=false") + } + + @Test + fun messageInitializerAndPrinter_isValidatedTrue_hasCorrectInfo() { + val model = + DefaultConnectionModel( + DefaultConnectionModel.Wifi(isDefault = true), + DefaultConnectionModel.Mobile(isDefault = false), + DefaultConnectionModel.CarrierMerged(isDefault = false), + DefaultConnectionModel.Ethernet(isDefault = false), + isValidated = true, + ) + val message = LogMessageImpl.create() + + model.messageInitializer(message) + val messageString = model.messagePrinter(message) + + assertThat(messageString).contains("wifi.isDefault=true") + assertThat(messageString).contains("mobile.isDefault=false") + assertThat(messageString).contains("carrierMerged.isDefault=false") + assertThat(messageString).contains("ethernet.isDefault=false") + assertThat(messageString).contains("isValidated=true") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt index 496f090da25cc..87d4f5c618a73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt @@ -16,31 +16,39 @@ package com.android.systemui.statusbar.pipeline.shared.data.repository +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.vcn.VcnTransportInfo +import android.net.wifi.WifiInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.pipeline.shared.ConnectivityInputLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.DEFAULT_HIDDEN_ICONS_RESOURCE import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.HIDDEN_ICONS_TUNABLE_KEY import com.android.systemui.tuner.TunerService import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock 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.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield -import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -50,51 +58,30 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: ConnectivityRepositoryImpl + @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var connectivitySlots: ConnectivitySlots @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var logger: ConnectivityInputLogger - private lateinit var scope: CoroutineScope + private lateinit var testScope: TestScope @Mock private lateinit var tunerService: TunerService @Before fun setUp() { MockitoAnnotations.initMocks(this) - scope = CoroutineScope(IMMEDIATE) - - underTest = - ConnectivityRepositoryImpl( - connectivitySlots, - context, - dumpManager, - logger, - scope, - tunerService, - ) - } - - @After - fun tearDown() { - scope.cancel() + testScope = TestScope(UnconfinedTestDispatcher()) + createAndSetRepo() } @Test fun forceHiddenSlots_initiallyGetsDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { setUpEthernetWifiMobileSlotNames() context .getOrCreateTestableResources() .addOverride(DEFAULT_HIDDEN_ICONS_RESOURCE, arrayOf(SLOT_WIFI, SLOT_ETHERNET)) // Re-create our [ConnectivityRepositoryImpl], since it fetches // config_statusBarIconsToExclude when it's first constructed - underTest = - ConnectivityRepositoryImpl( - connectivitySlots, - context, - dumpManager, - logger, - scope, - tunerService, - ) + createAndSetRepo() var latest: Set? = null val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this) @@ -106,7 +93,7 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_slotNamesAdded_flowHasSlots() = - runBlocking(IMMEDIATE) { + testScope.runTest { setUpEthernetWifiMobileSlotNames() var latest: Set? = null @@ -121,7 +108,7 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_wrongKey_doesNotUpdate() = - runBlocking(IMMEDIATE) { + testScope.runTest { setUpEthernetWifiMobileSlotNames() var latest: Set? = null @@ -141,22 +128,14 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_slotNamesAddedThenNull_flowHasDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { setUpEthernetWifiMobileSlotNames() context .getOrCreateTestableResources() .addOverride(DEFAULT_HIDDEN_ICONS_RESOURCE, arrayOf(SLOT_WIFI, SLOT_ETHERNET)) // Re-create our [ConnectivityRepositoryImpl], since it fetches // config_statusBarIconsToExclude when it's first constructed - underTest = - ConnectivityRepositoryImpl( - connectivitySlots, - context, - dumpManager, - logger, - scope, - tunerService, - ) + createAndSetRepo() var latest: Set? = null val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this) @@ -177,7 +156,7 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_someInvalidSlotNames_flowHasValidSlotsOnly() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Set? = null val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this) @@ -193,7 +172,7 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_someEmptySlotNames_flowHasValidSlotsOnly() = - runBlocking(IMMEDIATE) { + testScope.runTest { setUpEthernetWifiMobileSlotNames() var latest: Set? = null @@ -210,7 +189,7 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_allInvalidOrEmptySlotNames_flowHasEmpty() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Set? = null val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this) @@ -231,7 +210,7 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun forceHiddenSlots_newSubscriberGetsCurrentValue() = - runBlocking(IMMEDIATE) { + testScope.runTest { setUpEthernetWifiMobileSlotNames() var latest1: Set? = null @@ -252,9 +231,339 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { job2.cancel() } + @Test + fun defaultConnections_noTransports_nothingIsDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.mobile.isDefault).isFalse() + assertThat(latest!!.wifi.isDefault).isFalse() + assertThat(latest!!.ethernet.isDefault).isFalse() + assertThat(latest!!.carrierMerged.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_cellularTransport_mobileIsDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.mobile.isDefault).isTrue() + assertThat(latest!!.wifi.isDefault).isFalse() + assertThat(latest!!.ethernet.isDefault).isFalse() + assertThat(latest!!.carrierMerged.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_wifiTransport_wifiIsDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.wifi.isDefault).isTrue() + assertThat(latest!!.ethernet.isDefault).isFalse() + assertThat(latest!!.carrierMerged.isDefault).isFalse() + assertThat(latest!!.mobile.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_ethernetTransport_ethernetIsDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(true) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.ethernet.isDefault).isTrue() + assertThat(latest!!.wifi.isDefault).isFalse() + assertThat(latest!!.carrierMerged.isDefault).isFalse() + assertThat(latest!!.mobile.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_carrierMergedViaWifi_wifiAndCarrierMergedDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.wifi.isDefault).isTrue() + assertThat(latest!!.carrierMerged.isDefault).isTrue() + assertThat(latest!!.mobile.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_carrierMergedViaMobile_mobileCarrierMergedWifiDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.mobile.isDefault).isTrue() + assertThat(latest!!.carrierMerged.isDefault).isTrue() + assertThat(latest!!.wifi.isDefault).isTrue() + + job.cancel() + } + + @Test + fun defaultConnections_carrierMergedViaWifiWithVcnTransport_wifiAndCarrierMergedDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.wifi.isDefault).isTrue() + assertThat(latest!!.carrierMerged.isDefault).isTrue() + assertThat(latest!!.mobile.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_carrierMergedViaMobileWithVcnTransport_mobileCarrierMergedWifiDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.mobile.isDefault).isTrue() + assertThat(latest!!.carrierMerged.isDefault).isTrue() + assertThat(latest!!.wifi.isDefault).isTrue() + + job.cancel() + } + + @Test + fun defaultConnections_notCarrierMergedViaWifi_carrierMergedNotDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(false) } + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.carrierMerged.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_notCarrierMergedViaMobile_carrierMergedNotDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(false) } + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.carrierMerged.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_transportInfoNotWifi_wifiNotDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.transportInfo).thenReturn(mock()) + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.wifi.isDefault).isFalse() + + job.cancel() + } + + @Test + fun defaultConnections_multipleTransports_multipleDefault() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(true) + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.mobile.isDefault).isTrue() + assertThat(latest!!.ethernet.isDefault).isTrue() + assertThat(latest!!.wifi.isDefault).isTrue() + + job.cancel() + } + + @Test + fun defaultConnections_hasValidated_isValidatedTrue() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) + .thenReturn(true) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.isValidated).isTrue() + job.cancel() + } + + @Test + fun defaultConnections_noValidated_isValidatedFalse() = + testScope.runTest { + var latest: DefaultConnectionModel? = null + val job = underTest.defaultConnections.onEach { latest = it }.launchIn(this) + + val capabilities = + mock().also { + whenever(it.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) + .thenReturn(false) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest!!.isValidated).isFalse() + job.cancel() + } + + private fun createAndSetRepo() { + underTest = + ConnectivityRepositoryImpl( + connectivityManager, + connectivitySlots, + context, + dumpManager, + logger, + testScope.backgroundScope, + tunerService, + ) + } + private fun getTunable(): TunerService.Tunable { val callbackCaptor = argumentCaptor() - Mockito.verify(tunerService).addTunable(callbackCaptor.capture(), any()) + verify(tunerService).addTunable(callbackCaptor.capture(), any()) return callbackCaptor.value!! } @@ -265,10 +574,18 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(ConnectivitySlot.MOBILE) } - companion object { + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private companion object { private const val SLOT_ETHERNET = "ethernet" private const val SLOT_WIFI = "wifi" private const val SLOT_MOBILE = "mobile" - private val IMMEDIATE = Dispatchers.Main.immediate + + const val NETWORK_ID = 45 + val NETWORK = mock().apply { whenever(this.getNetId()).thenReturn(NETWORK_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt index bd70034b13de2..9e825b7048512 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.shared.data.repository import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,6 +27,9 @@ class FakeConnectivityRepository : ConnectivityRepository { MutableStateFlow(emptySet()) override val forceHiddenSlots: StateFlow> = _forceHiddenIcons + override val defaultConnections: StateFlow = + MutableStateFlow(DefaultConnectionModel()) + fun setForceHiddenIcons(hiddenIcons: Set) { _forceHiddenIcons.value = hiddenIcons } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt index 25678b0530f6e..70d2d2b68460f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel @@ -78,6 +79,7 @@ class WifiRepositorySwitcherTest : SysuiTestCase() { WifiRepositoryImpl( fakeBroadcastDispatcher, connectivityManager, + FakeConnectivityRepository(), logger, tableLogger, mainExecutor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index c7b31bcf9a0cb..f69e9a39909bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -34,7 +34,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -78,6 +81,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var wifiManager: WifiManager private lateinit var executor: Executor private lateinit var scope: CoroutineScope + private lateinit var connectivityRepository: ConnectivityRepository @Before fun setUp() { @@ -93,6 +97,18 @@ class WifiRepositoryImplTest : SysuiTestCase() { .thenReturn(flowOf(Unit)) executor = FakeExecutor(FakeSystemClock()) scope = CoroutineScope(IMMEDIATE) + + connectivityRepository = + ConnectivityRepositoryImpl( + connectivityManager, + ConnectivitySlots(context), + context, + mock(), + mock(), + scope, + mock(), + ) + underTest = createRepo() } @@ -302,13 +318,77 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test - fun isWifiDefault_cellularVcnNetwork_isTrue() = + fun isWifiDefault_carrierMergedViaCellular_isTrue() = + runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + + val capabilities = + mock().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + whenever(this.transportInfo).thenReturn(carrierMergedInfo) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_carrierMergedViaCellular_withVcnTransport_isTrue() = runBlocking(IMMEDIATE) { val job = underTest.isWifiDefault.launchIn(this) val capabilities = mock().apply { whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(false) + whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO)) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_carrierMergedViaWifi_isTrue() = + runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + + val capabilities = + mock().apply { + whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) + whenever(this.transportInfo).thenReturn(carrierMergedInfo) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_carrierMergedViaWifi_withVcnTransport_isTrue() = + runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = + mock().apply { + whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO)) } @@ -931,6 +1011,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { return WifiRepositoryImpl( broadcastDispatcher, connectivityManager, + connectivityRepository, logger, tableLogger, executor,