diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt index 59603874efdef..5562e73f04784 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.telephony.Annotation.NetworkType +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy /** @@ -38,4 +40,12 @@ sealed interface ResolvedNetworkType { data class OverrideNetworkType( override val lookupKey: String, ) : ResolvedNetworkType + + /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */ + object CarrierMergedNetworkType : ResolvedNetworkType { + // Effectively unused since [iconGroupOverride] is used instead. + override val lookupKey: String = "cwf" + + val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index d04996b4d6cee..6187f64e011d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -22,7 +22,6 @@ import android.telephony.TelephonyManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -50,7 +49,7 @@ interface MobileConnectionRepository { * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single * listener + model. */ - val connectionInfo: Flow + val connectionInfo: StateFlow /** The total number of levels. Used with [SignalDrawable]. */ val numberOfLevels: StateFlow 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 8ac12379e59e5..22aca0a8b0d7a 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 @@ -39,7 +39,11 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConn import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -60,15 +64,19 @@ import kotlinx.coroutines.launch class DemoMobileConnectionsRepository @Inject constructor( - private val dataSource: DemoModeMobileConnectionDataSource, + private val mobileDataSource: DemoModeMobileConnectionDataSource, + private val wifiDataSource: DemoModeWifiDataSource, @Application private val scope: CoroutineScope, context: Context, private val logFactory: TableLogBufferFactory, ) : MobileConnectionsRepository { - private var demoCommandJob: Job? = null + private var mobileDemoCommandJob: Job? = null + private var wifiDemoCommandJob: Job? = null - private var connectionRepoCache = mutableMapOf() + private var carrierMergedSubId: Int? = null + + private var connectionRepoCache = mutableMapOf() private val subscriptionInfoCache = mutableMapOf() val demoModeFinishedEvent = MutableSharedFlow(extraBufferCapacity = 1) @@ -144,52 +152,83 @@ constructor( override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { - return connectionRepoCache[subId] - ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it } + val current = connectionRepoCache[subId]?.repo + if (current != null) { + return current + } + + val new = createDemoMobileConnectionRepo(subId) + connectionRepoCache[subId] = new + return new.repo } - private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { - val tableLogBuffer = logFactory.getOrCreate("DemoMobileConnectionLog [$subId]", 100) + private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer { + val tableLogBuffer = + logFactory.getOrCreate( + "DemoMobileConnectionLog [$subId]", + MOBILE_CONNECTION_BUFFER_SIZE, + ) - return DemoMobileConnectionRepository( - subId, - tableLogBuffer, - ) + val repo = + DemoMobileConnectionRepository( + subId, + tableLogBuffer, + ) + return CacheContainer(repo, lastMobileState = null) } override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) fun startProcessingCommands() { - demoCommandJob = + mobileDemoCommandJob = scope.launch { - dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) } + mobileDataSource.mobileEvents.filterNotNull().collect { event -> + processMobileEvent(event) + } + } + wifiDemoCommandJob = + scope.launch { + wifiDataSource.wifiEvents.filterNotNull().collect { event -> + processWifiEvent(event) + } } } fun stopProcessingCommands() { - demoCommandJob?.cancel() + mobileDemoCommandJob?.cancel() + wifiDemoCommandJob?.cancel() _subscriptions.value = listOf() connectionRepoCache.clear() subscriptionInfoCache.clear() } - private fun processEvent(event: FakeNetworkEventModel) { + private fun processMobileEvent(event: FakeNetworkEventModel) { when (event) { is Mobile -> { processEnabledMobileState(event) } is MobileDisabled -> { - processDisabledMobileState(event) + maybeRemoveSubscription(event.subId) } } } + private fun processWifiEvent(event: FakeWifiEventModel) { + when (event) { + is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged() + is FakeWifiEventModel.Wifi -> disableCarrierMerged() + is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) + } + } + private fun processEnabledMobileState(state: Mobile) { // get or create the connection repo, and set its values val subId = state.subId ?: DEFAULT_SUB_ID maybeCreateSubscription(subId) val connection = getRepoForSubId(subId) + connectionRepoCache[subId]?.lastMobileState = state + // This is always true here, because we split out disabled states at the data-source level connection.dataEnabled.value = true connection.networkName.value = NetworkNameModel.Derived(state.name) @@ -198,14 +237,36 @@ constructor( connection.connectionInfo.value = state.toMobileConnectionModel() } - private fun processDisabledMobileState(state: MobileDisabled) { + private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { + // The new carrier merged connection is for a different sub ID, so disable carrier merged + // for the current (now old) sub + if (carrierMergedSubId != event.subscriptionId) { + disableCarrierMerged() + } + + // get or create the connection repo, and set its values + val subId = event.subscriptionId + maybeCreateSubscription(subId) + carrierMergedSubId = subId + + val connection = getRepoForSubId(subId) + // This is always true here, because we split out disabled states at the data-source level + connection.dataEnabled.value = true + connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME) + connection.numberOfLevels.value = event.numberOfLevels + connection.cdmaRoaming.value = false + connection.connectionInfo.value = event.toMobileConnectionModel() + Log.e("CCS", "output connection info = ${connection.connectionInfo.value}") + } + + private fun maybeRemoveSubscription(subId: Int?) { if (_subscriptions.value.isEmpty()) { // Nothing to do here return } - val subId = - state.subId + val finalSubId = + subId ?: run { // For sake of usability, we can allow for no subId arg if there is only one // subscription @@ -223,7 +284,21 @@ constructor( _subscriptions.value[0].subscriptionId } - removeSubscription(subId) + removeSubscription(finalSubId) + } + + private fun disableCarrierMerged() { + val currentCarrierMergedSubId = carrierMergedSubId ?: return + + // If this sub ID was previously not carrier merged, we should reset it to its previous + // connection. + val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState + if (lastMobileState != null) { + processEnabledMobileState(lastMobileState) + } else { + // Otherwise, just remove the subscription entirely + removeSubscription(currentCarrierMergedSubId) + } } private fun removeSubscription(subId: Int) { @@ -251,6 +326,10 @@ constructor( ) } + private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel { + return createCarrierMergedConnectionModel(this.level) + } + private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { val key = mobileMappingsReverseLookup.value[this] ?: "dis" return DefaultNetworkType(key) @@ -260,9 +339,17 @@ constructor( private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 + + private const val CARRIER_MERGED_NAME = "Carrier Merged Network" } } +class CacheContainer( + var repo: DemoMobileConnectionRepository, + /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */ + var lastMobileState: Mobile?, +) + class DemoMobileConnectionRepository( override val subId: Int, override val tableLogBuffer: TableLogBuffer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt new file mode 100644 index 0000000000000..c783b12e0c0b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +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 +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is + * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually + * displayed as a mobile network triangle. + * + * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. + * + * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile + * connection. + */ +class CarrierMergedConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, + defaultNetworkName: NetworkNameModel, + @Application private val scope: CoroutineScope, + val wifiRepository: WifiRepository, +) : MobileConnectionRepository { + + /** + * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged + * network. + */ + private val network: Flow = + combine( + wifiRepository.isWifiEnabled, + wifiRepository.isWifiDefault, + wifiRepository.wifiNetwork, + ) { isEnabled, isDefault, network -> + when { + !isEnabled -> null + !isDefault -> null + network !is WifiNetworkModel.CarrierMerged -> null + network.subscriptionId != subId -> { + Log.w( + TAG, + "Connection repo subId=$subId " + + "does not equal wifi repo subId=${network.subscriptionId}; " + + "not showing carrier merged" + ) + null + } + else -> network + } + } + + override val connectionInfo: StateFlow = + network + .map { it.toMobileConnectionModel() } + .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel()) + + // TODO(b/238425913): Add logging to this class. + // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate. + + // Carrier merged is never roaming. + override val cdmaRoaming: StateFlow = MutableStateFlow(false).asStateFlow() + + // TODO(b/238425913): Fetch the carrier merged network name. + override val networkName: StateFlow = + flowOf(defaultNetworkName) + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) + + override val numberOfLevels: StateFlow = + wifiRepository.wifiNetwork + .map { + if (it is WifiNetworkModel.CarrierMerged) { + it.numberOfLevels + } else { + DEFAULT_NUM_LEVELS + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) + + override val dataEnabled: StateFlow = wifiRepository.isWifiEnabled + + private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel { + if (this == null) { + return MobileConnectionModel() + } + + return createCarrierMergedConnectionModel(level) + } + + companion object { + /** + * Creates an instance of [MobileConnectionModel] that represents a carrier merged network + * with the given [level]. + */ + fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel { + return MobileConnectionModel( + primaryLevel = level, + cdmaLevel = level, + // A [WifiNetworkModel.CarrierMerged] instance is always connected. + // (A [WifiNetworkModel.Inactive] represents a disconnected network.) + dataConnectionState = DataConnectionState.Connected, + // TODO(b/238425913): This should come from [WifiRepository.wifiActivity]. + dataActivityDirection = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), + resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, + // Carrier merged is never roaming + isRoaming = false, + + // TODO(b/238425913): Verify that these fields never change for carrier merged. + isEmergencyOnly = false, + operatorAlphaShort = null, + isInService = true, + isGsm = false, + carrierNetworkChangeActive = false, + ) + } + } + + @SysUISingleton + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + private val wifiRepository: WifiRepository, + ) { + fun build( + subId: Int, + mobileLogger: TableLogBuffer, + defaultNetworkName: NetworkNameModel, + ): MobileConnectionRepository { + return CarrierMergedConnectionRepository( + subId, + mobileLogger, + defaultNetworkName, + scope, + wifiRepository, + ) + } + } +} + +private const val TAG = "CarrierMergedConnectionRepository" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt new file mode 100644 index 0000000000000..0f30ae249c31b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * A repository that fully implements a mobile connection. + * + * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl] + * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository + * switches between the two types of connections based on whether the connection is currently + * carrier merged (see [setIsCarrierMerged]). + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class FullMobileConnectionRepository( + override val subId: Int, + startingIsCarrierMerged: Boolean, + override val tableLogBuffer: TableLogBuffer, + private val defaultNetworkName: NetworkNameModel, + private val networkNameSeparator: String, + private val globalMobileDataSettingChangedEvent: Flow, + @Application scope: CoroutineScope, + private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory, + private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory, +) : MobileConnectionRepository { + /** + * Sets whether this connection is a typical mobile connection or a carrier merged connection. + */ + fun setIsCarrierMerged(isCarrierMerged: Boolean) { + _isCarrierMerged.value = isCarrierMerged + } + + /** + * Returns true if this repo is currently for a carrier merged connection and false otherwise. + */ + @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value + + private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged) + private val isCarrierMerged: StateFlow = + _isCarrierMerged + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = "isCarrierMerged", + initialValue = startingIsCarrierMerged, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged) + + private val mobileRepo: MobileConnectionRepository by lazy { + mobileRepoFactory.build( + subId, + tableLogBuffer, + defaultNetworkName, + networkNameSeparator, + globalMobileDataSettingChangedEvent, + ) + } + + private val carrierMergedRepo: MobileConnectionRepository by lazy { + carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName) + } + + @VisibleForTesting + internal val activeRepo: StateFlow = run { + val initial = + if (startingIsCarrierMerged) { + carrierMergedRepo + } else { + mobileRepo + } + + this.isCarrierMerged + .mapLatest { isCarrierMerged -> + if (isCarrierMerged) { + carrierMergedRepo + } else { + mobileRepo + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } + + override val cdmaRoaming = + activeRepo + .flatMapLatest { it.cdmaRoaming } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value) + + override val connectionInfo = + activeRepo + .flatMapLatest { it.connectionInfo } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value) + + override val dataEnabled = + activeRepo + .flatMapLatest { it.dataEnabled } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value) + + override val numberOfLevels = + activeRepo + .flatMapLatest { it.numberOfLevels } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value) + + override val networkName = + activeRepo + .flatMapLatest { it.networkName } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value) + + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + private val logFactory: TableLogBufferFactory, + private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory, + private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory, + ) { + fun build( + subId: Int, + startingIsCarrierMerged: Boolean, + defaultNetworkName: NetworkNameModel, + networkNameSeparator: String, + globalMobileDataSettingChangedEvent: Flow, + ): FullMobileConnectionRepository { + val mobileLogger = + logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE) + + return FullMobileConnectionRepository( + subId, + startingIsCarrierMerged, + mobileLogger, + defaultNetworkName, + networkNameSeparator, + globalMobileDataSettingChangedEvent, + scope, + mobileRepoFactory, + carrierMergedRepoFactory, + ) + } + + companion object { + /** The buffer size to use for logging. */ + const val MOBILE_CONNECTION_BUFFER_SIZE = 100 + + /** Returns a log buffer name for a mobile connection with the given [subId]. */ + fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 4e42f9b31e5c0..3f2ce4000ff1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -38,7 +38,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -70,6 +69,10 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +/** + * A repository implementation for a typical mobile connection (as opposed to a carrier merged + * connection -- see [CarrierMergedConnectionRepository]). + */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( @@ -298,18 +301,16 @@ class MobileConnectionRepositoryImpl( private val logger: ConnectivityPipelineLogger, private val globalSettings: GlobalSettings, private val mobileMappingsProxy: MobileMappingsProxy, - private val logFactory: TableLogBufferFactory, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { fun build( subId: Int, + mobileLogger: TableLogBuffer, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, globalMobileDataSettingChangedEvent: Flow, ): MobileConnectionRepository { - val mobileLogger = logFactory.getOrCreate(tableBufferLogName(subId), 100) - return MobileConnectionRepositoryImpl( context, subId, @@ -327,8 +328,4 @@ class MobileConnectionRepositoryImpl( ) } } - - companion object { - fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" - } } 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 c88c70064238c..4472e0972a0b0 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 @@ -46,11 +46,12 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel 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 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -85,9 +86,14 @@ constructor( private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, - private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory + // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi + // repository is an input to the mobile repository. + // See [CarrierMergedConnectionRepository] for details. + wifiRepository: WifiRepository, + private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory, ) : MobileConnectionsRepository { - private var subIdRepositoryCache: MutableMap = mutableMapOf() + private var subIdRepositoryCache: MutableMap = + mutableMapOf() private val defaultNetworkName = NetworkNameModel.Default( @@ -97,30 +103,43 @@ constructor( private val networkNameSeparator: String = context.getString(R.string.status_bar_network_name_separator) + private val carrierMergedSubId: StateFlow = + wifiRepository.wifiNetwork + .mapLatest { + if (it is WifiNetworkModel.CarrierMerged) { + it.subscriptionId + } else { + null + } + } + .distinctUntilChanged() + .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) + + private val mobileSubscriptionsChangeEvent: Flow = conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + /** * State flow that emits the set of mobile data subscriptions, each represented by its own - * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each - * info object, but for now we keep track of the infos themselves. + * [SubscriptionModel]. */ override val subscriptions: StateFlow> = - conflatedCallbackFlow { - val callback = - object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - bgDispatcher.asExecutor(), - callback, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } - } + merge(mobileSubscriptionsChangeEvent, carrierMergedSubId) .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } .logInputChange(logger, "onSubscriptionsChanged") - .onEach { infos -> dropUnusedReposFromCache(infos) } + .onEach { infos -> updateRepos(infos) } .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) /** StateFlow that keeps track of the current active mobile data subscription */ @@ -173,7 +192,7 @@ constructor( .distinctUntilChanged() .logInputChange(logger, "defaultMobileIconGroup") - override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository { if (!isValidSubId(subId)) { throw IllegalArgumentException( "subscriptionId $subId is not in the list of valid subscriptions" @@ -251,15 +270,27 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache - private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build( + private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository { + return fullMobileRepoFactory.build( subId, + isCarrierMerged(subId), defaultNetworkName, networkNameSeparator, globalMobileDataSettingChangedEvent, ) } + private fun updateRepos(newInfos: List) { + dropUnusedReposFromCache(newInfos) + subIdRepositoryCache.forEach { (subId, repo) -> + repo.setIsCarrierMerged(isCarrierMerged(subId)) + } + } + + private fun isCarrierMerged(subId: Int): Boolean { + return subId == carrierMergedSubId.value + } + private fun dropUnusedReposFromCache(newInfos: List) { // Remove any connection repository from the cache that isn't in the new set of IDs. They // will get garbage collected once their subscribers go away 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 9427c6b9fece2..003df2482c6e4 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,8 +22,8 @@ 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.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -138,7 +138,11 @@ class MobileIconInteractorImpl( defaultMobileIconMapping, defaultMobileIconGroup, ) { info, mapping, defaultGroup -> - mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + when (info.resolvedNetworkType) { + is ResolvedNetworkType.CarrierMergedNetworkType -> + info.resolvedNetworkType.iconGroupOverride + else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + } } .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt index 4251d18357f7d..da2daf2c55ea8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt @@ -16,13 +16,18 @@ package com.android.systemui.statusbar.pipeline.wifi.data.model +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.annotation.VisibleForTesting import com.android.systemui.log.table.TableRowLogger import com.android.systemui.log.table.Diffable +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS /** Provides information about the current wifi network. */ sealed class WifiNetworkModel : Diffable { + // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of + // copy-pasting the column names for each sub-object. + /** * A model representing that we couldn't fetch any wifi information. * @@ -41,8 +46,43 @@ sealed class WifiNetworkModel : Diffable { override fun logFull(row: TableRowLogger) { row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE) row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) row.logChange(COL_VALIDATED, false) row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) + } + } + + /** + * A model representing that the wifi information we received was invalid in some way. + */ + data class Invalid( + /** A description of why the wifi information was invalid. */ + val invalidReason: String, + ) : WifiNetworkModel() { + override fun toString() = "WifiNetwork.Invalid[$invalidReason]" + override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { + if (prevVal !is Invalid) { + logFull(row) + return + } + + if (invalidReason != prevVal.invalidReason) { + row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason") + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) row.logChange(COL_SSID, null) row.logChange(COL_PASSPOINT_ACCESS_POINT, false) row.logChange(COL_ONLINE_SIGN_UP, false) @@ -59,18 +99,21 @@ sealed class WifiNetworkModel : Diffable { return } - if (prevVal is CarrierMerged) { - // The only difference between CarrierMerged and Inactive is the type - row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) - return - } - - // When changing from Active to Inactive, we need to log diffs to all the fields. - logFullNonActiveNetwork(TYPE_INACTIVE, row) + // When changing to Inactive, we need to log diffs to all the fields. + logFull(row) } override fun logFull(row: TableRowLogger) { - logFullNonActiveNetwork(TYPE_INACTIVE, row) + row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -80,22 +123,75 @@ sealed class WifiNetworkModel : Diffable { * * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. */ - object CarrierMerged : WifiNetworkModel() { - override fun toString() = "WifiNetwork.CarrierMerged" + data class CarrierMerged( + /** + * The [android.net.Network.netId] we received from + * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network. + * + * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId]. + */ + val networkId: Int, + + /** + * The subscription ID that this connection represents. + * + * Comes from [android.net.wifi.WifiInfo.getSubscriptionId]. + * + * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid, + * then this is *not* a carrier merged network). + */ + val subscriptionId: Int, + + /** + * The signal level, guaranteed to be 0 <= level <= numberOfLevels. + */ + val level: Int, + + /** + * The maximum possible level. + */ + val numberOfLevels: Int = DEFAULT_NUM_LEVELS, + ) : WifiNetworkModel() { + init { + require(level in MIN_VALID_LEVEL..numberOfLevels) { + "0 <= wifi level <= $numberOfLevels required; level was $level" + } + require(subscriptionId != INVALID_SUBSCRIPTION_ID) { + "subscription ID cannot be invalid" + } + } override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { - if (prevVal is CarrierMerged) { + if (prevVal !is CarrierMerged) { + logFull(row) return } - if (prevVal is Inactive) { - // The only difference between CarrierMerged and Inactive is the type. - row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) - return + if (prevVal.networkId != networkId) { + row.logChange(COL_NETWORK_ID, networkId) } + if (prevVal.subscriptionId != subscriptionId) { + row.logChange(COL_SUB_ID, subscriptionId) + } + if (prevVal.level != level) { + row.logChange(COL_LEVEL, level) + } + if (prevVal.numberOfLevels != numberOfLevels) { + row.logChange(COL_NUM_LEVELS, numberOfLevels) + } + } - // When changing from Active to CarrierMerged, we need to log diffs to all the fields. - logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row) + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) + row.logChange(COL_NETWORK_ID, networkId) + row.logChange(COL_SUB_ID, subscriptionId) + row.logChange(COL_VALIDATED, true) + row.logChange(COL_LEVEL, level) + row.logChange(COL_NUM_LEVELS, numberOfLevels) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -137,38 +233,50 @@ sealed class WifiNetworkModel : Diffable { override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { if (prevVal !is Active) { - row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) + logFull(row) + return } - if (prevVal !is Active || prevVal.networkId != networkId) { + if (prevVal.networkId != networkId) { row.logChange(COL_NETWORK_ID, networkId) } - if (prevVal !is Active || prevVal.isValidated != isValidated) { + if (prevVal.isValidated != isValidated) { row.logChange(COL_VALIDATED, isValidated) } - if (prevVal !is Active || prevVal.level != level) { + if (prevVal.level != level) { row.logChange(COL_LEVEL, level) } - if (prevVal !is Active || prevVal.ssid != ssid) { + if (prevVal.ssid != ssid) { row.logChange(COL_SSID, ssid) } // TODO(b/238425913): The passpoint-related values are frequently never used, so it // would be great to not log them when they're not used. - if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) { + if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) { row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint) } - if (prevVal !is Active || - prevVal.isOnlineSignUpForPasspointAccessPoint != + if (prevVal.isOnlineSignUpForPasspointAccessPoint != isOnlineSignUpForPasspointAccessPoint) { row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint) } - if (prevVal !is Active || - prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) { + if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) { row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName) } } + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) + row.logChange(COL_NETWORK_ID, networkId) + row.logChange(COL_SUB_ID, null) + row.logChange(COL_VALIDATED, isValidated) + row.logChange(COL_LEVEL, level) + row.logChange(COL_NUM_LEVELS, null) + row.logChange(COL_SSID, ssid) + row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint) + row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint) + row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName) + } + override fun toString(): String { // Only include the passpoint-related values in the string if we have them. (Most // networks won't have them so they'll be mostly clutter.) @@ -188,22 +296,14 @@ sealed class WifiNetworkModel : Diffable { } companion object { - @VisibleForTesting - internal const val MIN_VALID_LEVEL = 0 @VisibleForTesting internal const val MAX_VALID_LEVEL = 4 } } - internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) { - row.logChange(COL_NETWORK_TYPE, type) - row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) - row.logChange(COL_VALIDATED, false) - row.logChange(COL_LEVEL, LEVEL_DEFAULT) - row.logChange(COL_SSID, null) - row.logChange(COL_PASSPOINT_ACCESS_POINT, false) - row.logChange(COL_ONLINE_SIGN_UP, false) - row.logChange(COL_PASSPOINT_NAME, null) + companion object { + @VisibleForTesting + internal const val MIN_VALID_LEVEL = 0 } } @@ -214,12 +314,16 @@ const val TYPE_ACTIVE = "Active" const val COL_NETWORK_TYPE = "type" const val COL_NETWORK_ID = "networkId" +const val COL_SUB_ID = "subscriptionId" const val COL_VALIDATED = "isValidated" const val COL_LEVEL = "level" +const val COL_NUM_LEVELS = "maxLevel" const val COL_SSID = "ssid" const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint" const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint" const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName" val LEVEL_DEFAULT: String? = null +val NUM_LEVELS_DEFAULT: String? = null val NETWORK_ID_DEFAULT: String? = null +val SUB_ID_DEFAULT: String? = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt index c588945fbd677..caac8fa2f2c3d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,10 +44,10 @@ constructor( private fun Bundle.toWifiEvent(): FakeWifiEventModel? { val wifi = getString("wifi") ?: return null - return if (wifi == "show") { - activeWifiEvent() - } else { - FakeWifiEventModel.WifiDisabled + return when (wifi) { + "show" -> activeWifiEvent() + "carriermerged" -> carrierMergedWifiEvent() + else -> FakeWifiEventModel.WifiDisabled } } @@ -64,6 +65,14 @@ constructor( ) } + private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged { + val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID + val level = getString("level")?.toInt() ?: 0 + val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS + + return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels) + } + private fun String.toActivity(): Int = when (this) { "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT @@ -71,4 +80,8 @@ constructor( "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE } + + companion object { + const val DEFAULT_CARRIER_MERGED_SUB_ID = 10 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt index be3d7d4e65c41..e161b3e42d02c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -66,6 +66,7 @@ constructor( private fun processEvent(event: FakeWifiEventModel) = when (event) { is FakeWifiEventModel.Wifi -> processEnabledWifiState(event) + is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState() } @@ -85,6 +86,14 @@ constructor( _wifiNetwork.value = event.toWifiNetworkModel() } + private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { + _isWifiEnabled.value = true + _isWifiDefault.value = true + // TODO(b/238425913): Support activity in demo mode. + _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = event.toCarrierMergedModel() + } + private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel = WifiNetworkModel.Active( networkId = DEMO_NET_ID, @@ -99,6 +108,14 @@ constructor( passpointProviderFriendlyName = null, ) + private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel = + WifiNetworkModel.CarrierMerged( + networkId = DEMO_NET_ID, + subscriptionId = subscriptionId, + level = level, + numberOfLevels = numberOfLevels, + ) + companion object { private const val DEMO_NET_ID = 1234 } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt index 2353fb82f3b1b..518f8ce66d2e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt @@ -29,5 +29,11 @@ sealed interface FakeWifiEventModel { val validated: Boolean?, ) : FakeWifiEventModel + data class CarrierMerged( + val subscriptionId: Int, + val level: Int, + val numberOfLevels: Int, + ) : FakeWifiEventModel + object WifiDisabled : FakeWifiEventModel } 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 c47c20d280c7d..d26499c18661d 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 @@ -29,6 +29,7 @@ import android.net.NetworkRequest 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 @@ -269,7 +270,19 @@ constructor( wifiManager: WifiManager, ): WifiNetworkModel { return if (wifiInfo.isCarrierMerged) { - WifiNetworkModel.CarrierMerged + if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) { + WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON) + } else { + WifiNetworkModel.CarrierMerged( + networkId = network.getNetId(), + subscriptionId = wifiInfo.subscriptionId, + level = wifiManager.calculateSignalLevel(wifiInfo.rssi), + // The WiFi signal level returned by WifiManager#calculateSignalLevel start + // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level + // buckets count. + numberOfLevels = wifiManager.maxSignalLevel + 1, + ) + } } else { WifiNetworkModel.Active( network.getNetId(), @@ -302,6 +315,9 @@ constructor( .build() private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel" + + private const val CARRIER_MERGED_INVALID_SUB_ID_REASON = + "Wifi network was carrier merged but had invalid sub ID" } @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 980560ab5d581..86dcd18c643cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -66,6 +66,7 @@ class WifiInteractorImpl @Inject constructor( override val ssid: Flow = wifiRepository.wifiNetwork.map { info -> when (info) { is WifiNetworkModel.Unavailable -> null + is WifiNetworkModel.Invalid -> null is WifiNetworkModel.Inactive -> null is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.Active -> when { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 824b5972ba4be..95431afb71bb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -83,6 +83,7 @@ constructor( private fun WifiNetworkModel.icon(): WifiIcon { return when (this) { is WifiNetworkModel.Unavailable -> WifiIcon.Hidden + is WifiNetworkModel.Invalid -> WifiIcon.Hidden is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden is WifiNetworkModel.Inactive -> WifiIcon.Visible( res = WIFI_NO_NETWORK, 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 5d377a8658a57..0859d140c3b4f 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 @@ -34,6 +34,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.valid 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.ConnectivityPipelineLogger +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 import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock @@ -71,8 +73,10 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private lateinit var underTest: MobileRepositorySwitcher private lateinit var realRepo: MobileConnectionsRepositoryImpl private lateinit var demoRepo: DemoMobileConnectionsRepository - private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource + private lateinit var wifiDataSource: DemoModeWifiDataSource private lateinit var logFactory: TableLogBufferFactory + private lateinit var wifiRepository: FakeWifiRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @@ -96,10 +100,15 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { // Never start in demo mode whenever(demoModeController.isInDemoMode).thenReturn(false) - mockDataSource = + mobileDataSource = mock().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow) } + wifiDataSource = + mock().also { + whenever(it.wifiEvents).thenReturn(MutableStateFlow(null)) + } + wifiRepository = FakeWifiRepository() realRepo = MobileConnectionsRepositoryImpl( @@ -113,12 +122,14 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { context, IMMEDIATE, scope, + wifiRepository, mock(), ) demoRepo = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mobileDataSource, + wifiDataSource = wifiDataSource, scope = scope, context = context, logFactory = logFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index 210208532dd45..6989b514a703c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -29,6 +29,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectio import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -63,10 +65,12 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC private val testScope = TestScope(testDispatcher) private val fakeNetworkEventFlow = MutableStateFlow(null) + private val fakeWifiEventFlow = MutableStateFlow(null) private lateinit var connectionsRepo: DemoMobileConnectionsRepository private lateinit var underTest: DemoMobileConnectionRepository private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mockWifiDataSource: DemoModeWifiDataSource @Before fun setUp() { @@ -75,10 +79,15 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC mock().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) } + mockWifiDataSource = + mock().also { + whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow) + } connectionsRepo = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mockDataSource, + wifiDataSource = mockWifiDataSource, scope = testScope.backgroundScope, context = context, logFactory = logFactory, 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 cdbe75e855bcb..9d16b7fe52460 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 @@ -32,6 +32,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionMod import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -57,21 +59,28 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) private val fakeNetworkEventFlow = MutableStateFlow(null) + private val fakeWifiEventFlow = MutableStateFlow(null) private lateinit var underTest: DemoMobileConnectionsRepository - private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource + private lateinit var wifiDataSource: DemoModeWifiDataSource @Before fun setUp() { // The data source only provides one API, so we can mock it with a flow here for convenience - mockDataSource = + mobileDataSource = mock().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) } + wifiDataSource = + mock().also { + whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow) + } underTest = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mobileDataSource, + wifiDataSource = wifiDataSource, scope = testScope.backgroundScope, context = context, logFactory = logFactory, @@ -96,6 +105,22 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `wifi carrier merged event - create new subscription`() = + testScope.runTest { + var latest: List? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + job.cancel() + } + @Test fun `network event - reuses subscription when same Id`() = testScope.runTest { @@ -118,6 +143,28 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `wifi carrier merged event - reuses subscription when same Id`() = + testScope.runTest { + var latest: List? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + // Second network event comes in with the same subId, does not create a new subscription + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + job.cancel() + } + @Test fun `multiple subscriptions`() = testScope.runTest { @@ -132,6 +179,35 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `mobile subscription and carrier merged subscription`() = + testScope.runTest { + var latest: List? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + + assertThat(latest).hasSize(2) + + job.cancel() + } + + @Test + fun `multiple mobile subscriptions and carrier merged subscription`() = + testScope.runTest { + var latest: List? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeNetworkEventFlow.value = validMobileEvent(subId = 2) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3) + + assertThat(latest).hasSize(3) + + job.cancel() + } + @Test fun `mobile disabled event - disables connection - subId specified - single conn`() = testScope.runTest { @@ -194,6 +270,112 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `wifi network updates to disabled - carrier merged connection removed`() = + testScope.runTest { + var latest: List? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + + assertThat(latest).hasSize(1) + + fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled + + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun `wifi network updates to active - carrier merged connection removed`() = + testScope.runTest { + var latest: List? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + + assertThat(latest).hasSize(1) + + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi( + level = 1, + activity = 0, + ssid = null, + validated = true, + ) + + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun `mobile sub updates to carrier merged - only one connection`() = + testScope.runTest { + var latestSubsList: List? = null + var connections: List? = null + val job = + underTest.subscriptions + .onEach { latestSubsList = it } + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2) + assertThat(latestSubsList).hasSize(1) + + val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1) + fakeWifiEventFlow.value = carrierMergedEvent + assertThat(latestSubsList).hasSize(1) + val connection = connections!!.find { it.subId == 3 }!! + assertCarrierMergedConnection(connection, carrierMergedEvent) + + job.cancel() + } + + @Test + fun `mobile sub updates to carrier merged then back - has old mobile data`() = + testScope.runTest { + var latestSubsList: List? = null + var connections: List? = null + val job = + underTest.subscriptions + .onEach { latestSubsList = it } + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + val mobileEvent = validMobileEvent(subId = 3, level = 2) + fakeNetworkEventFlow.value = mobileEvent + assertThat(latestSubsList).hasSize(1) + + val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1) + fakeWifiEventFlow.value = carrierMergedEvent + assertThat(latestSubsList).hasSize(1) + var connection = connections!!.find { it.subId == 3 }!! + assertCarrierMergedConnection(connection, carrierMergedEvent) + + // WHEN the carrier merged is removed + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi( + level = 4, + activity = 0, + ssid = null, + validated = true, + ) + + // THEN the subId=3 connection goes back to the mobile information + connection = connections!!.find { it.subId == 3 }!! + assertConnection(connection, mobileEvent) + + job.cancel() + } + /** Regression test for b/261706421 */ @Test fun `multiple connections - remove all - does not throw`() = @@ -289,6 +471,51 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `demo connection - two connections - update carrier merged - no affect on first`() = + testScope.runTest { + var currentEvent1 = validMobileEvent(subId = 1) + var connection1: DemoMobileConnectionRepository? = null + var currentEvent2 = validCarrierMergedEvent(subId = 2) + var connection2: DemoMobileConnectionRepository? = null + var connections: List? = null + val job = + underTest.subscriptions + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = currentEvent1 + fakeWifiEventFlow.value = currentEvent2 + assertThat(connections).hasSize(2) + connections!!.forEach { + when (it.subId) { + 1 -> connection1 = it + 2 -> connection2 = it + else -> Assert.fail("Unexpected subscription") + } + } + + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + // WHEN the event changes for connection 2, it updates, and connection 1 stays the same + currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4) + fakeWifiEventFlow.value = currentEvent2 + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + // and vice versa + currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true) + fakeNetworkEventFlow.value = currentEvent1 + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + job.cancel() + } + private fun assertConnection( conn: DemoMobileConnectionRepository, model: FakeNetworkEventModel @@ -315,6 +542,21 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { else -> {} } } + + private fun assertCarrierMergedConnection( + conn: DemoMobileConnectionRepository, + model: FakeWifiEventModel.CarrierMerged, + ) { + val connectionInfo: MobileConnectionModel = conn.connectionInfo.value + assertThat(conn.subId).isEqualTo(model.subscriptionId) + assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) + assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) + assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false) + assertThat(connectionInfo.isRoaming).isEqualTo(false) + assertThat(connectionInfo.isEmergencyOnly).isFalse() + assertThat(connectionInfo.isGsm).isFalse() + assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected) + } } /** Convenience to create a valid fake network event with minimal params */ @@ -339,3 +581,14 @@ fun validMobileEvent( roaming = roaming, name = "demo name", ) + +fun validCarrierMergedEvent( + subId: Int = 1, + level: Int = 1, + numberOfLevels: Int = 4, +): FakeWifiEventModel.CarrierMerged = + FakeWifiEventModel.CarrierMerged( + subscriptionId = subId, + level = level, + numberOfLevels = numberOfLevels, + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt new file mode 100644 index 0000000000000..ea90150b432a5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +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.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: CarrierMergedConnectionRepository + + private lateinit var wifiRepository: FakeWifiRepository + @Mock private lateinit var logger: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + wifiRepository = FakeWifiRepository() + + underTest = + CarrierMergedConnectionRepository( + SUB_ID, + logger, + NetworkNameModel.Default("name"), + testScope.backgroundScope, + wifiRepository, + ) + } + + @Test + fun connectionInfo_inactiveWifi_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun connectionInfo_activeWifi_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1)) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setIsWifiEnabled(true) + wifiRepository.setIsWifiDefault(true) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + + val expected = + MobileConnectionModel( + primaryLevel = 3, + cdmaLevel = 3, + dataConnectionState = DataConnectionState.Connected, + dataActivityDirection = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), + resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, + isRoaming = false, + isEmergencyOnly = false, + operatorAlphaShort = null, + isInService = true, + isGsm = false, + carrierNetworkChangeActive = false, + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID + 10, + level = 3, + ) + ) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest!!.primaryLevel).isNotEqualTo(3) + assertThat(latest!!.resolvedNetworkType) + .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + + job.cancel() + } + + // This scenario likely isn't possible, but write a test for it anyway + @Test + fun connectionInfo_carrierMergedButNotEnabled_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + wifiRepository.setIsWifiEnabled(false) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + // This scenario likely isn't possible, but write a test for it anyway + @Test + fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + wifiRepository.setIsWifiDefault(false) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun numberOfLevels_comesFromCarrierMerged() = + testScope.runTest { + var latest: Int? = null + val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 1, + numberOfLevels = 6, + ) + ) + + assertThat(latest).isEqualTo(6) + + job.cancel() + } + + @Test + fun dataEnabled_matchesWifiEnabled() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + + wifiRepository.setIsWifiEnabled(true) + assertThat(latest).isTrue() + + wifiRepository.setIsWifiEnabled(false) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun cdmaRoaming_alwaysFalse() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + private companion object { + const val SUB_ID = 123 + const val NET_ID = 456 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt new file mode 100644 index 0000000000000..c02a4dfd074cd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -0,0 +1,389 @@ +/* + * 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.mobile.data.repository.prod + +import androidx.test.filters.SmallTest +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.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +/** + * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the + * repository interface it's switching on. These tests just need to verify that the entire interface + * properly switches over when the value of `isCarrierMerged` changes. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class FullMobileConnectionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: FullMobileConnectionRepository + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val mobileMappings = FakeMobileMappingsProxy() + private val tableLogBuffer = mock() + private val mobileFactory = mock() + private val carrierMergedFactory = mock() + + private lateinit var connectionsRepo: FakeMobileConnectionsRepository + private val globalMobileDataSettingChangedEvent: Flow + get() = connectionsRepo.globalMobileDataSettingChangedEvent + + private lateinit var mobileRepo: FakeMobileConnectionRepository + private lateinit var carrierMergedRepo: FakeMobileConnectionRepository + + @Before + fun setUp() { + connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer) + + mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + + whenever( + mobileFactory.build( + eq(SUB_ID), + any(), + eq(DEFAULT_NAME), + eq(SEP), + eq(globalMobileDataSettingChangedEvent), + ) + ) + .thenReturn(mobileRepo) + whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME))) + .thenReturn(carrierMergedRepo) + } + + @Test + fun startingIsCarrierMerged_usesCarrierMergedInitially() = + testScope.runTest { + val carrierMergedConnectionInfo = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + ) + carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo) + + initializeRepo(startingIsCarrierMerged = true) + + assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo) + assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo) + verify(mobileFactory, never()) + .build( + SUB_ID, + tableLogBuffer, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent + ) + } + + @Test + fun startingNotCarrierMerged_usesTypicalInitially() = + testScope.runTest { + val mobileConnectionInfo = + MobileConnectionModel( + operatorAlphaShort = "Typical Operator", + ) + mobileRepo.setConnectionInfo(mobileConnectionInfo) + + initializeRepo(startingIsCarrierMerged = false) + + assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) + assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo) + verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME) + } + + @Test + fun activeRepo_matchesIsCarrierMerged() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + var latest: MobileConnectionRepository? = null + val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(true) + + assertThat(latest).isEqualTo(carrierMergedRepo) + + underTest.setIsCarrierMerged(false) + + assertThat(latest).isEqualTo(mobileRepo) + + underTest.setIsCarrierMerged(true) + + assertThat(latest).isEqualTo(carrierMergedRepo) + + job.cancel() + } + + @Test + fun connectionInfo_getsUpdatesFromRepo_carrierMerged() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(true) + + val info1 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + primaryLevel = 1, + ) + carrierMergedRepo.setConnectionInfo(info1) + + assertThat(latest).isEqualTo(info1) + + val info2 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator #2", + primaryLevel = 2, + ) + carrierMergedRepo.setConnectionInfo(info2) + + assertThat(latest).isEqualTo(info2) + + val info3 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator #3", + primaryLevel = 3, + ) + carrierMergedRepo.setConnectionInfo(info3) + + assertThat(latest).isEqualTo(info3) + + job.cancel() + } + + @Test + fun connectionInfo_getsUpdatesFromRepo_mobile() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(false) + + val info1 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator", + primaryLevel = 1, + ) + mobileRepo.setConnectionInfo(info1) + + assertThat(latest).isEqualTo(info1) + + val info2 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator #2", + primaryLevel = 2, + ) + mobileRepo.setConnectionInfo(info2) + + assertThat(latest).isEqualTo(info2) + + val info3 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator #3", + primaryLevel = 3, + ) + mobileRepo.setConnectionInfo(info3) + + assertThat(latest).isEqualTo(info3) + + job.cancel() + } + + @Test + fun connectionInfo_updatesWhenCarrierMergedUpdates() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + primaryLevel = 4, + ) + carrierMergedRepo.setConnectionInfo(carrierMergedInfo) + + val mobileInfo = + MobileConnectionModel( + operatorAlphaShort = "Typical Operator", + primaryLevel = 2, + ) + mobileRepo.setConnectionInfo(mobileInfo) + + // Start with the mobile info + assertThat(latest).isEqualTo(mobileInfo) + + // WHEN isCarrierMerged is set to true + underTest.setIsCarrierMerged(true) + + // THEN the carrier merged info is used + assertThat(latest).isEqualTo(carrierMergedInfo) + + val newCarrierMergedInfo = + MobileConnectionModel( + operatorAlphaShort = "New CM Operator", + primaryLevel = 0, + ) + carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo) + + assertThat(latest).isEqualTo(newCarrierMergedInfo) + + // WHEN isCarrierMerged is set to false + underTest.setIsCarrierMerged(false) + + // THEN the typical info is used + assertThat(latest).isEqualTo(mobileInfo) + + val newMobileInfo = + MobileConnectionModel( + operatorAlphaShort = "New Mobile Operator", + primaryLevel = 3, + ) + mobileRepo.setConnectionInfo(newMobileInfo) + + assertThat(latest).isEqualTo(newMobileInfo) + + job.cancel() + } + + @Test + fun `factory - reuses log buffers for same connection`() = + testScope.runTest { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + val factory = + FullMobileConnectionRepository.Factory( + scope = testScope.backgroundScope, + realLoggerFactory, + mobileFactory, + carrierMergedFactory, + ) + + // Create two connections for the same subId. Similar to if the connection appeared + // and disappeared from the connectionFactory's perspective + val connection1 = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + val connection1Repeat = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1Repeat.tableLogBuffer) + } + + @Test + fun `factory - reuses log buffers for same sub ID even if carrier merged`() = + testScope.runTest { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + val factory = + FullMobileConnectionRepository.Factory( + scope = testScope.backgroundScope, + realLoggerFactory, + mobileFactory, + carrierMergedFactory, + ) + + val connection1 = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + // WHEN a connection with the same sub ID but carrierMerged = true is created + val connection1Repeat = + factory.build( + SUB_ID, + startingIsCarrierMerged = true, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + // THEN the same table is re-used + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1Repeat.tableLogBuffer) + } + + // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo + // implements logging). + + private fun initializeRepo(startingIsCarrierMerged: Boolean) { + underTest = + FullMobileConnectionRepository( + SUB_ID, + startingIsCarrierMerged, + tableLogBuffer, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + testScope.backgroundScope, + mobileFactory, + carrierMergedFactory, + ) + } + + private companion object { + const val SUB_ID = 42 + private val DEFAULT_NAME = NetworkNameModel.Default("default name") + private const val SEP = "-" + } +} 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 09589707b331d..813b0ed041a75 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 @@ -37,17 +37,18 @@ 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.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.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings -import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -74,6 +75,9 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory + private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory + private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory + private lateinit var wifiRepository: FakeWifiRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @@ -100,6 +104,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { mock() } + wifiRepository = FakeWifiRepository() + connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, @@ -110,7 +116,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { logger = logger, mobileMappingsProxy = mobileMappings, scope = scope, + ) + carrierMergedFactory = + CarrierMergedConnectionRepository.Factory( + scope, + wifiRepository, + ) + fullConnectionFactory = + FullMobileConnectionRepository.Factory( + scope = scope, logFactory = logBufferFactory, + mobileRepoFactory = connectionFactory, + carrierMergedRepoFactory = carrierMergedFactory, ) underTest = @@ -125,7 +142,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) } @@ -179,6 +197,40 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = + runBlocking(IMMEDIATE) { + var latest: List? = null + + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(MODEL_CM)) + + job.cancel() + } + + @Test + fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = + runBlocking(IMMEDIATE) { + var latest: List? = null + + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM)) + + job.cancel() + } + @Test fun testActiveDataSubscriptionId_initialValueIsInvalidId() = runBlocking(IMMEDIATE) { @@ -218,6 +270,96 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testConnectionRepository_carrierMergedSubId_isCached() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_CM_ID) + val repo2 = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(repo1).isSameInstanceAs(repo2) + + job.cancel() + } + + @Test + fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + val mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test + fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be not carrier merged + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1)) + + // THEN the repos update + val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test + fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be carrier merged + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + + // THEN the repos update + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + @Test fun testConnectionCache_clearsInvalidSubscriptions() = runBlocking(IMMEDIATE) { @@ -244,6 +386,34 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged) + + // SUB_2 and SUB_CM disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + + job.cancel() + } + /** Regression test for b/261706421 */ @Test fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = @@ -295,59 +465,19 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { underTest.getRepoForSubId(SUB_1_ID) verify(logBufferFactory) .getOrCreate( - eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), + eq(tableBufferLogName(SUB_1_ID)), anyInt(), ) underTest.getRepoForSubId(SUB_2_ID) verify(logBufferFactory) .getOrCreate( - eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), + eq(tableBufferLogName(SUB_2_ID)), anyInt(), ) job.cancel() } - @Test - fun `connection repository factory - reuses log buffers for same connection`() = - runBlocking(IMMEDIATE) { - val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) - - connectionFactory = - MobileConnectionRepositoryImpl.Factory( - fakeBroadcastDispatcher, - context = context, - telephonyManager = telephonyManager, - bgDispatcher = IMMEDIATE, - globalSettings = globalSettings, - logger = logger, - mobileMappingsProxy = mobileMappings, - scope = scope, - logFactory = realLoggerFactory, - ) - - // Create two connections for the same subId. Similar to if the connection appeared - // and disappeared from the connectionFactory's perspective - val connection1 = - connectionFactory.build( - 1, - NetworkNameModel.Default("default_name"), - "-", - underTest.globalMobileDataSettingChangedEvent, - ) - - val connection1_repeat = - connectionFactory.build( - 1, - NetworkNameModel.Default("default_name"), - "-", - underTest.globalMobileDataSettingChangedEvent, - ) - - assertThat(connection1.tableLogBuffer) - .isSameInstanceAs(connection1_repeat.tableLogBuffer) - } - @Test fun mobileConnectivity_default() { assertThat(underTest.defaultMobileNetworkConnectivity.value) @@ -461,7 +591,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) var latest: MobileMappings.Config? = null @@ -571,5 +702,16 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private const val NET_ID = 123 private val NETWORK = mock().apply { whenever(getNetId()).thenReturn(NET_ID) } + + private const val SUB_CM_ID = 5 + private val SUB_CM = + mock().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) } + private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID) + private val WIFI_NETWORK_CM = + WifiNetworkModel.CarrierMerged( + networkId = 3, + subscriptionId = SUB_CM_ID, + level = 1, + ) } } 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 61e13b85db6c7..e6be7f15235b3 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 @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -270,6 +271,23 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun iconGroup_carrierMerged_usesOverride() = + runBlocking(IMMEDIATE) { + connectionRepository.setConnectionInfo( + MobileConnectionModel( + resolvedNetworkType = CarrierMergedNetworkType, + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride) + + job.cancel() + } + @Test fun alwaysShowDataRatIcon_matchesParent() = runBlocking(IMMEDIATE) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt index 30ac8d432e8ac..824cebdc3c080 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.pipeline.wifi.data.model +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -44,8 +45,52 @@ class WifiNetworkModelTest : SysuiTestCase() { WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1) } + @Test(expected = IllegalArgumentException::class) + fun carrierMerged_invalidSubId_exceptionThrown() { + WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1) + } + // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken + @Test + fun logDiffs_carrierMergedToInactive_resetsAllFields() { + val logger = TestLogger() + val prevVal = + WifiNetworkModel.CarrierMerged( + networkId = 5, + subscriptionId = 3, + level = 1, + ) + + WifiNetworkModel.Inactive.logDiffs(prevVal, logger) + + assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE)) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_SSID, "null")) + } + + @Test + fun logDiffs_inactiveToCarrierMerged_logsAllFields() { + val logger = TestLogger() + val carrierMerged = + WifiNetworkModel.CarrierMerged( + networkId = 6, + subscriptionId = 3, + level = 2, + ) + + carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger) + + assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6")) + assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3")) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, "2")) + assertThat(logger.changes).contains(Pair(COL_SSID, "null")) + } + @Test fun logDiffs_inactiveToActive_logsAllActiveFields() { val logger = TestLogger() @@ -95,8 +140,14 @@ class WifiNetworkModelTest : SysuiTestCase() { level = 3, ssid = "Test SSID" ) + val prevVal = + WifiNetworkModel.CarrierMerged( + networkId = 5, + subscriptionId = 3, + level = 1, + ) - activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger) + activeNetwork.logDiffs(prevVal, logger) assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE)) assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5")) @@ -105,7 +156,7 @@ class WifiNetworkModelTest : SysuiTestCase() { assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID")) } @Test - fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() { + fun logDiffs_activeToCarrierMerged_logsAllFields() { val logger = TestLogger() val activeNetwork = WifiNetworkModel.Active( @@ -114,13 +165,20 @@ class WifiNetworkModelTest : SysuiTestCase() { level = 3, ssid = "Test SSID" ) + val carrierMerged = + WifiNetworkModel.CarrierMerged( + networkId = 6, + subscriptionId = 3, + level = 2, + ) - WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger) + carrierMerged.logDiffs(prevVal = activeNetwork, logger) assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)) - assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString())) - assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false")) - assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6")) + assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3")) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, "2")) assertThat(logger.changes).contains(Pair(COL_SSID, "null")) } 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 8f07615b19b28..87ce8faff5a56 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 @@ -26,6 +26,7 @@ import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher @@ -340,7 +341,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { .launchIn(this) val wifiInfo = mock().apply { - whenever(this.ssid).thenReturn(SSID) whenever(this.isPrimary).thenReturn(true) whenever(this.isCarrierMerged).thenReturn(true) } @@ -352,6 +352,67 @@ class WifiRepositoryImplTest : SysuiTestCase() { job.cancel() } + @Test + fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = + runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + } + + getNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java) + + job.cancel() + } + + @Test + fun wifiNetwork_isCarrierMerged_getsCorrectValues() = + runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val rssi = -57 + val wifiInfo = mock().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.rssi).thenReturn(rssi) + whenever(this.subscriptionId).thenReturn(567) + } + + whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2) + whenever(wifiManager.maxSignalLevel).thenReturn(5) + + getNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged + assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID) + assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567) + assertThat(latestCarrierMerged.level).isEqualTo(2) + // numberOfLevels = maxSignalLevel + 1 + assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6) + + job.cancel() + } + @Test fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt index 01d59f96c2219..089a170aa2bee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt @@ -84,7 +84,9 @@ class WifiInteractorImplTest : SysuiTestCase() { @Test fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1) + ) var latest: String? = "default" val job = underTest diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index 726e813ec4141..b9328377772aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -206,7 +206,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // Enabled = false => no networks shown TestCase( enabled = false, - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), TestCase( @@ -228,7 +229,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // forceHidden = true => no networks shown TestCase( forceHidden = true, - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), TestCase( @@ -369,7 +371,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // network = CarrierMerged => not shown TestCase( - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ),