diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index b9c4030d9d0ee..a822e185479a7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -600,6 +600,9 @@ public class Utils { * Returns the WifiInfo for the underlying WiFi network of the VCN network, returns null if the * input NetworkCapabilities is not for a VCN network with underlying WiFi network. * + * TODO(b/238425913): Move this method to be inside systemui not settingslib once we've migrated + * off of {@link WifiStatusTracker} and {@link NetworkControllerImpl}. + * * @param networkCapabilities NetworkCapabilities of the network. */ @Nullable diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt deleted file mode 100644 index 780a02da34103..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline - -import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo -import kotlinx.coroutines.flow.StateFlow - -/** - * Interface exposing a flow for raw connectivity information. Clients should collect on - * [rawConnectivityInfoFlow] to get updates on connectivity information. - * - * Note: [rawConnectivityInfoFlow] should be a *hot* flow, so that we only create one instance of it - * and all clients get references to the same flow. - * - * This will be used for the new status bar pipeline to compile information we need to display some - * of the icons in the RHS of the status bar. - */ -interface ConnectivityInfoCollector { - val rawConnectivityInfoFlow: StateFlow -} - -/** - * An object containing all of the raw connectivity information. - * - * Importantly, all the information in this object should not be processed at all (i.e., the data - * that we receive from callbacks should be piped straight into this object and not be filtered, - * manipulated, or processed in any way). Instead, any listeners on - * [ConnectivityInfoCollector.rawConnectivityInfoFlow] can do the processing. - * - * This allows us to keep all the processing in one place which is beneficial for logging and - * debugging purposes. - */ -data class RawConnectivityInfo( - val networkCapabilityInfo: Map = emptyMap(), -) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt deleted file mode 100644 index 99798f9b38d3a..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilitiesRepo -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -/** - * The real implementation of [ConnectivityInfoCollector] that will collect information from all the - * relevant connectivity callbacks and compile it into [rawConnectivityInfoFlow]. - */ -@SysUISingleton -class ConnectivityInfoCollectorImpl @Inject constructor( - networkCapabilitiesRepo: NetworkCapabilitiesRepo, - @Application scope: CoroutineScope, -) : ConnectivityInfoCollector { - override val rawConnectivityInfoFlow: StateFlow = - // TODO(b/238425913): Collect all the separate flows for individual raw information into - // this final flow. - networkCapabilitiesRepo.dataStream - .map { - RawConnectivityInfo(networkCapabilityInfo = it) - } - .stateIn(scope, started = Lazily, initialValue = RawConnectivityInfo()) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt index 64c47f6791424..0b018168fbd52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt @@ -20,31 +20,21 @@ import android.content.Context import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** - * A processor that transforms raw connectivity information that we get from callbacks and turns it - * into a list of displayable connectivity information. + * A temporary object that collects on [WifiViewModel] flows for debugging purposes. * - * This will be used for the new status bar pipeline to calculate the list of icons that should be - * displayed in the RHS of the status bar. - * - * Anyone can listen to [processedInfoFlow] to get updates to the processed data. + * This will eventually get migrated to a view binder that will use the flow outputs to set state on + * views. For now, this just collects on flows so that the information gets logged. */ @SysUISingleton class ConnectivityInfoProcessor @Inject constructor( - connectivityInfoCollector: ConnectivityInfoCollector, context: Context, // TODO(b/238425913): Don't use the application scope; instead, use the status bar view's // scope so we only do work when there's UI that cares about it. @@ -52,21 +42,6 @@ class ConnectivityInfoProcessor @Inject constructor( private val statusBarPipelineFlags: StatusBarPipelineFlags, private val wifiViewModelProvider: Provider, ) : CoreStartable(context) { - // Note: This flow will not start running until a client calls `collect` on it, which means that - // [connectivityInfoCollector]'s flow will also not start anything until that `collect` call - // happens. - // TODO(b/238425913): Delete this. - val processedInfoFlow: Flow = - if (!statusBarPipelineFlags.isNewPipelineEnabled()) - emptyFlow() - else connectivityInfoCollector.rawConnectivityInfoFlow - .map { it.process() } - .stateIn( - scope, - started = Lazily, - initialValue = ProcessedConnectivityInfo() - ) - override fun start() { if (!statusBarPipelineFlags.isNewPipelineEnabled()) { return @@ -77,17 +52,4 @@ class ConnectivityInfoProcessor @Inject constructor( wifiViewModelProvider.get().isActivityInVisible.collect { } } } - - private fun RawConnectivityInfo.process(): ProcessedConnectivityInfo { - // TODO(b/238425913): Actually process the raw info into meaningful data. - return ProcessedConnectivityInfo(this.networkCapabilityInfo) - } } - -/** - * An object containing connectivity info that has been processed into data that can be directly - * used by the status bar (and potentially other SysUI areas) to display icons. - */ -data class ProcessedConnectivityInfo( - val networkCapabilityInfo: Map = emptyMap(), -) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 7abe19e7bbe0a..88eccb5ba47fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.pipeline.dagger import com.android.systemui.CoreStartable -import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollector -import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollectorImpl import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl @@ -35,11 +33,6 @@ abstract class StatusBarPipelineModule { @ClassKey(ConnectivityInfoProcessor::class) abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable - @Binds - abstract fun provideConnectivityInfoCollector( - impl: ConnectivityInfoCollectorImpl - ): ConnectivityInfoCollector - @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index a5fff5e65ebc2..2a89309f72001 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -31,6 +31,9 @@ import kotlinx.coroutines.flow.onEach class ConnectivityPipelineLogger @Inject constructor( @StatusBarConnectivityLog private val buffer: LogBuffer, ) { + /** + * Logs a change in one of the **raw inputs** to the connectivity pipeline. + */ fun logInputChange(callbackName: String, changeInfo: String) { buffer.log( SB_LOGGING_TAG, @@ -45,6 +48,41 @@ class ConnectivityPipelineLogger @Inject constructor( ) } + /** + * Logs a **data transformation** that we performed within the connectivity pipeline. + */ + fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) { + if (oldValue == newValue) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + str1 = transformationName + str2 = oldValue.toString() + }, + { + "Transform: $str1: $str2 (transformation didn't change it)" + } + ) + } else { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + str1 = transformationName + str2 = oldValue.toString() + str3 = newValue.toString() + }, + { + "Transform: $str1: $str2 -> $str3" + } + ) + } + } + + /** + * Logs a change in one of the **outputs** to the connectivity pipeline. + */ fun logOutputChange(outputParamName: String, changeInfo: String) { buffer.log( SB_LOGGING_TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt deleted file mode 100644 index 1b7332265b7a2..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.wifi.data.model - -/** Provides information about the current wifi state. */ -data class WifiModel( - /** See [android.net.wifi.WifiInfo.ssid]. */ - val ssid: String? = null, - /** See [android.net.wifi.WifiInfo.isPasspointAp]. */ - val isPasspointAccessPoint: Boolean = false, - /** See [android.net.wifi.WifiInfo.isOsuAp]. */ - val isOnlineSignUpForPasspointAccessPoint: Boolean = false, - /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */ - val passpointProviderFriendlyName: String? = null, -) 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 new file mode 100644 index 0000000000000..5566fa6b58354 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt @@ -0,0 +1,46 @@ +/* + * 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.wifi.data.model + +/** Provides information about the current wifi network. */ +sealed class WifiNetworkModel { + /** A model representing that we have no active wifi network. */ + object Inactive : WifiNetworkModel() + + /** Provides information about an active wifi network. */ + class Active( + /** + * 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, + + /** See [android.net.wifi.WifiInfo.ssid]. */ + val ssid: String? = null, + + /** See [android.net.wifi.WifiInfo.isPasspointAp]. */ + val isPasspointAccessPoint: Boolean = false, + + /** See [android.net.wifi.WifiInfo.isOsuAp]. */ + val isOnlineSignUpForPasspointAccessPoint: Boolean = false, + + /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */ + val passpointProviderFriendlyName: String? = null, + ) : WifiNetworkModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt deleted file mode 100644 index 6c0a44524e3a1..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2021 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. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.systemui.statusbar.pipeline.wifi.data.repository - -import android.annotation.SuppressLint -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkRequest -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.stateIn - -/** - * Repository that contains all relevant [NetworkCapabilities] for the current networks. - * - * TODO(b/238425913): Figure out how to merge this with [WifiRepository]. - */ -@SysUISingleton -class NetworkCapabilitiesRepo @Inject constructor( - connectivityManager: ConnectivityManager, - @Application scope: CoroutineScope, - logger: ConnectivityPipelineLogger, -) { - @SuppressLint("MissingPermission") - val dataStream: StateFlow> = run { - var state = emptyMap() - callbackFlow { - val networkRequest: NetworkRequest = - NetworkRequest.Builder() - .clearCapabilities() - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .build() - val callback = - // TODO (b/240569788): log these using [LogBuffer] - object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - logger.logOnCapabilitiesChanged(network, networkCapabilities) - state = - state.toMutableMap().also { - it[network.getNetId()] = - NetworkCapabilityInfo(network, networkCapabilities) - } - trySend(state) - } - - override fun onLost(network: Network) { - logger.logOnLost(network) - state = state.toMutableMap().also { it.remove(network.getNetId()) } - trySend(state) - } - } - connectivityManager.registerNetworkCallback(networkRequest, callback) - - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } - .stateIn(scope, started = Lazily, initialValue = state) - } -} - -/** contains info about network capabilities. */ -data class NetworkCapabilityInfo( - val network: Network, - val capabilities: NetworkCapabilities, -) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 012dde5fb8f2c..43fbabca823f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -16,16 +16,26 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository +import android.annotation.SuppressLint +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkRequest +import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback import android.util.Log +import com.android.settingslib.Utils import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -38,9 +48,9 @@ import kotlinx.coroutines.flow.flowOf */ interface WifiRepository { /** - * Observable for the current state of wifi; `null` when there is no active wifi. + * Observable for the current wifi network. */ - val wifiModel: Flow + val wifiNetwork: Flow /** * Observable for the current wifi network activity. @@ -51,14 +61,57 @@ interface WifiRepository { /** Real implementation of [WifiRepository]. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton +@SuppressLint("MissingPermission") class WifiRepositoryImpl @Inject constructor( + connectivityManager: ConnectivityManager, wifiManager: WifiManager?, @Main mainExecutor: Executor, logger: ConnectivityPipelineLogger, ) : WifiRepository { + override val wifiNetwork: Flow = conflatedCallbackFlow { + var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT - // TODO(b/238425913): Actually implement the wifiModel flow. - override val wifiModel: Flow = flowOf(WifiModel(ssid = "AB")) + val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + logger.logOnCapabilitiesChanged(network, networkCapabilities) + + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + if (wifiInfo?.isPrimary == true) { + val wifiNetworkModel = wifiInfoToModel(wifiInfo, network.getNetId()) + logger.logTransformation( + WIFI_NETWORK_CALLBACK_NAME, + oldValue = currentWifi, + newValue = wifiNetworkModel + ) + currentWifi = wifiNetworkModel + trySend(wifiNetworkModel) + } + } + + override fun onLost(network: Network) { + logger.logOnLost(network) + val wifi = currentWifi + if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) { + val newNetworkModel = WifiNetworkModel.Inactive + logger.logTransformation( + WIFI_NETWORK_CALLBACK_NAME, + oldValue = wifi, + newValue = newNetworkModel + ) + currentWifi = newNetworkModel + trySend(newNetworkModel) + } + } + } + + trySend(WIFI_NETWORK_DEFAULT) + connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback) + + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } override val wifiActivity: Flow = if (wifiManager == null) { @@ -71,8 +124,8 @@ class WifiRepositoryImpl @Inject constructor( trySend(trafficStateToWifiActivityModel(state)) } - wifiManager.registerTrafficStateCallback(mainExecutor, callback) trySend(ACTIVITY_DEFAULT) + wifiManager.registerTrafficStateCallback(mainExecutor, callback) awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } } @@ -80,6 +133,13 @@ class WifiRepositoryImpl @Inject constructor( companion object { val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false) + // Start out with no known wifi network. + // Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an + // initial fetch to get a starting wifi network. But, it uses a deprecated API + // [WifiManager.getConnectionInfo()], and the deprecation doc indicates to just use + // [ConnectivityManager.NetworkCallback] results instead. So, for now we'll just rely on the + // NetworkCallback inside [wifiNetwork] for our wifi network information. + val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel { return WifiActivityModel( @@ -90,6 +150,30 @@ class WifiRepositoryImpl @Inject constructor( ) } + private fun networkCapabilitiesToWifiInfo( + networkCapabilities: NetworkCapabilities + ): WifiInfo? { + return when { + networkCapabilities.hasTransport(TRANSPORT_WIFI) -> + networkCapabilities.transportInfo as WifiInfo? + networkCapabilities.hasTransport(TRANSPORT_CELLULAR) -> + // Sometimes, cellular networks can act as wifi networks (known as VCN -- + // virtual carrier network). So, see if this cellular network has wifi info. + Utils.tryGetWifiInfoForVcn(networkCapabilities) + else -> null + } + } + + private fun wifiInfoToModel(wifiInfo: WifiInfo, networkId: Int): WifiNetworkModel { + return WifiNetworkModel.Active( + networkId, + wifiInfo.ssid, + wifiInfo.isPasspointAp, + wifiInfo.isOsuAp, + wifiInfo.passpointProviderFriendlyName + ) + } + private fun prettyPrintActivity(activity: Int): String { return when (activity) { TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE" @@ -99,5 +183,15 @@ class WifiRepositoryImpl @Inject constructor( else -> "INVALID" } } + + private val WIFI_NETWORK_CALLBACK_REQUEST: NetworkRequest = + NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_WIFI) + .addTransportType(TRANSPORT_CELLULAR) + .build() + + private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel" } } 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 f705399af85d3..a9da63b43aa4d 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 @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor import android.net.wifi.WifiManager import com.android.systemui.dagger.SysUISingleton +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.flow.Flow @@ -34,13 +35,15 @@ import kotlinx.coroutines.flow.map class WifiInteractor @Inject constructor( repository: WifiRepository, ) { - private val ssid: Flow = repository.wifiModel.map { info -> - when { - info == null -> null - info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint -> - info.passpointProviderFriendlyName - info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid - else -> null + private val ssid: Flow = repository.wifiNetwork.map { info -> + when (info) { + is WifiNetworkModel.Inactive -> null + is WifiNetworkModel.Active -> when { + info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint -> + info.passpointProviderFriendlyName + info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid + else -> null + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt deleted file mode 100644 index 7b492cb7ddd17..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline - -import android.net.NetworkCapabilities -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo -import com.android.systemui.util.mockito.mock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.yield -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.`when` as whenever - -@OptIn(InternalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidTestingRunner::class) -class ConnectivityInfoProcessorTest : SysuiTestCase() { - - private val statusBarPipelineFlags = mock() - - @Before - fun setUp() { - whenever(statusBarPipelineFlags.isNewPipelineEnabled()).thenReturn(true) - } - - @Test - fun collectorInfoUpdated_processedInfoAlsoUpdated() = runBlocking { - // GIVEN a processor hooked up to a collector - val scope = CoroutineScope(Dispatchers.Unconfined) - val collector = FakeConnectivityInfoCollector() - val processor = ConnectivityInfoProcessor( - collector, - context, - scope, - statusBarPipelineFlags, - mock(), - ) - - var mostRecentValue: ProcessedConnectivityInfo? = null - val job = launch(start = CoroutineStart.UNDISPATCHED) { - processor.processedInfoFlow.collect { - mostRecentValue = it - } - } - - // WHEN the collector emits a value - val networkCapabilityInfo = mapOf( - 10 to NetworkCapabilityInfo(mock(), NetworkCapabilities.Builder().build()) - ) - collector.emitValue(RawConnectivityInfo(networkCapabilityInfo)) - // Because our job uses [CoroutineStart.UNDISPATCHED], it executes in the same thread as - // this test. So, our test needs to yield to let the job run. - // Note: Once we upgrade our Kotlin coroutines testing library, we won't need this. - yield() - - // THEN the processor receives it - assertThat(mostRecentValue?.networkCapabilityInfo).isEqualTo(networkCapabilityInfo) - - job.cancel() - scope.cancel() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt deleted file mode 100644 index 710e5f6eacd36..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -/** - * A test-friendly implementation of [ConnectivityInfoCollector] that just emits whatever value it - * receives in [emitValue]. - */ -class FakeConnectivityInfoCollector : ConnectivityInfoCollector { - private val _rawConnectivityInfoFlow = MutableStateFlow(RawConnectivityInfo()) - override val rawConnectivityInfoFlow = _rawConnectivityInfoFlow.asStateFlow() - - suspend fun emitValue(value: RawConnectivityInfo) { - _rawConnectivityInfoFlow.emit(value) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt index df389bc526646..6b8d4aa7c51f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt @@ -17,21 +17,22 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow /** Fake implementation of [WifiRepository] exposing set methods for all the flows. */ class FakeWifiRepository : WifiRepository { - private val _wifiModel: MutableStateFlow = MutableStateFlow(null) - override val wifiModel: Flow = _wifiModel + private val _wifiNetwork: MutableStateFlow = + MutableStateFlow(WifiNetworkModel.Inactive) + override val wifiNetwork: Flow = _wifiNetwork private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT) override val wifiActivity: Flow = _wifiActivity - fun setWifiModel(wifiModel: WifiModel?) { - _wifiModel.value = wifiModel + fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) { + _wifiNetwork.value = wifiNetworkModel } fun setWifiActivity(activity: WifiActivityModel) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt deleted file mode 100644 index 6edf76ce77c95..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.wifi.data.repository - -import android.net.ConnectivityManager -import android.net.ConnectivityManager.NetworkCallback -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED -import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED -import android.net.NetworkCapabilities.TRANSPORT_CELLULAR -import android.net.NetworkCapabilities.TRANSPORT_WIFI -import android.net.NetworkRequest -import android.test.suitebuilder.annotation.SmallTest -import android.testing.AndroidTestingRunner -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.withArgCaptor -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -// TODO(b/240619365): Update this test to use `runTest` when we update the testing library -@SmallTest -@RunWith(AndroidTestingRunner::class) -class NetworkCapabilitiesRepoTest : SysuiTestCase() { - @Mock private lateinit var connectivityManager: ConnectivityManager - @Mock private lateinit var logger: ConnectivityPipelineLogger - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - } - - @Test - fun testOnCapabilitiesChanged_oneNewNetwork_networkStored() = runBlocking { - // GIVEN a repo hooked up to [ConnectivityManager] - val scope = CoroutineScope(Dispatchers.Unconfined) - val repo = NetworkCapabilitiesRepo( - connectivityManager = connectivityManager, - scope = scope, - logger = logger, - ) - - val job = launch(start = CoroutineStart.UNDISPATCHED) { - repo.dataStream.collect { - } - } - - val callback: NetworkCallback = withArgCaptor { - verify(connectivityManager) - .registerNetworkCallback(any(NetworkRequest::class.java), capture()) - } - - // WHEN a new network is added - callback.onCapabilitiesChanged(NET_1, NET_1_CAPS) - - val currentMap = repo.dataStream.value - - // THEN it is emitted from the flow - assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1) - assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS) - - job.cancel() - scope.cancel() - } - - @Test - fun testOnCapabilitiesChanged_twoNewNetworks_bothStored() = runBlocking { - // GIVEN a repo hooked up to [ConnectivityManager] - val scope = CoroutineScope(Dispatchers.Unconfined) - val repo = NetworkCapabilitiesRepo( - connectivityManager = connectivityManager, - scope = scope, - logger = logger, - ) - - val job = launch(start = CoroutineStart.UNDISPATCHED) { - repo.dataStream.collect { - } - } - - val callback: NetworkCallback = withArgCaptor { - verify(connectivityManager) - .registerNetworkCallback(any(NetworkRequest::class.java), capture()) - } - - // WHEN two new networks are added - callback.onCapabilitiesChanged(NET_1, NET_1_CAPS) - callback.onCapabilitiesChanged(NET_2, NET_2_CAPS) - - val currentMap = repo.dataStream.value - - // THEN the current state of the flow reflects 2 networks - assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1) - assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS) - assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2) - assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS) - - job.cancel() - scope.cancel() - } - - @Test - fun testOnCapabilitesChanged_newCapabilitiesForExistingNetwork_areCaptured() = runBlocking { - // GIVEN a repo hooked up to [ConnectivityManager] - val scope = CoroutineScope(Dispatchers.Unconfined) - val repo = NetworkCapabilitiesRepo( - connectivityManager = connectivityManager, - scope = scope, - logger = logger, - ) - - val job = launch(start = CoroutineStart.UNDISPATCHED) { - repo.dataStream.collect { - } - } - - val callback: NetworkCallback = withArgCaptor { - verify(connectivityManager) - .registerNetworkCallback(any(NetworkRequest::class.java), capture()) - } - - // WHEN a network is added, and then its capabilities are changed - callback.onCapabilitiesChanged(NET_1, NET_1_CAPS) - callback.onCapabilitiesChanged(NET_1, NET_2_CAPS) - - val currentMap = repo.dataStream.value - - // THEN the current state of the flow reflects the new capabilities - assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_2_CAPS) - - job.cancel() - scope.cancel() - } - - @Test - fun testOnLost_networkIsRemoved() = runBlocking { - // GIVEN a repo hooked up to [ConnectivityManager] - val scope = CoroutineScope(Dispatchers.Unconfined) - val repo = NetworkCapabilitiesRepo( - connectivityManager = connectivityManager, - scope = scope, - logger = logger, - ) - - val job = launch(start = CoroutineStart.UNDISPATCHED) { - repo.dataStream.collect { - } - } - - val callback: NetworkCallback = withArgCaptor { - verify(connectivityManager) - .registerNetworkCallback(any(NetworkRequest::class.java), capture()) - } - - // WHEN two new networks are added, and one is removed - callback.onCapabilitiesChanged(NET_1, NET_1_CAPS) - callback.onCapabilitiesChanged(NET_2, NET_2_CAPS) - callback.onLost(NET_1) - - val currentMap = repo.dataStream.value - - // THEN the current state of the flow reflects only the remaining network - assertThat(currentMap[NET_1_ID]).isNull() - assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2) - assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS) - - job.cancel() - scope.cancel() - } - - @Test - fun testOnLost_noNetworks_doesNotCrash() = runBlocking { - // GIVEN a repo hooked up to [ConnectivityManager] - val scope = CoroutineScope(Dispatchers.Unconfined) - val repo = NetworkCapabilitiesRepo( - connectivityManager = connectivityManager, - scope = scope, - logger = logger, - ) - - val job = launch(start = CoroutineStart.UNDISPATCHED) { - repo.dataStream.collect { - } - } - - val callback: NetworkCallback = withArgCaptor { - verify(connectivityManager) - .registerNetworkCallback(any(NetworkRequest::class.java), capture()) - } - - // WHEN no networks are added, and one is removed - callback.onLost(NET_1) - - val currentMap = repo.dataStream.value - - // THEN the current state of the flow shows no networks - assertThat(currentMap).isEmpty() - - job.cancel() - scope.cancel() - } - - private val NET_1_ID = 100 - private val NET_1 = mock().also { - whenever(it.getNetId()).thenReturn(NET_1_ID) - } - private val NET_2_ID = 200 - private val NET_2 = mock().also { - whenever(it.getNetId()).thenReturn(NET_2_ID) - } - - private val NET_1_CAPS = NetworkCapabilities.Builder() - .addTransportType(TRANSPORT_CELLULAR) - .addCapability(NET_CAPABILITY_VALIDATED) - .build() - - private val NET_2_CAPS = NetworkCapabilities.Builder() - .addTransportType(TRANSPORT_WIFI) - .addCapability(NET_CAPABILITY_NOT_METERED) - .addCapability(NET_CAPABILITY_VALIDATED) - .build() -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt index 8b61364a2ac92..d0a38084af761 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt @@ -16,16 +16,26 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.vcn.VcnTransportInfo +import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor @@ -38,6 +48,7 @@ import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @@ -47,6 +58,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: WifiRepositoryImpl @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var wifiManager: WifiManager private lateinit var executor: Executor @@ -54,11 +66,347 @@ class WifiRepositoryImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(FakeSystemClock()) + + underTest = WifiRepositoryImpl( + connectivityManager, + wifiManager, + executor, + logger, + ) + } + + @Test + fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT) + + job.cancel() + } + + @Test + fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock().apply { + whenever(this.ssid).thenReturn(SSID) + whenever(this.isPrimary).thenReturn(true) + } + val network = mock().apply { + whenever(this.getNetId()).thenReturn(NETWORK_ID) + } + + getNetworkCallback().onCapabilitiesChanged(network, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) + assertThat(latestActive.ssid).isEqualTo(SSID) + + job.cancel() + } + + @Test + fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock().apply { + whenever(this.ssid).thenReturn(SSID) + whenever(this.isPrimary).thenReturn(false) + } + + getNetworkCallback().onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + + job.cancel() + } + + @Test + fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val capabilities = mock().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO)) + } + + getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) + assertThat(latestActive.ssid).isEqualTo(SSID) + + job.cancel() + } + + @Test + fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock().apply { + whenever(this.ssid).thenReturn(SSID) + whenever(this.isPrimary).thenReturn(false) + } + val capabilities = mock().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(VcnTransportInfo(wifiInfo)) + } + + getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + + job.cancel() + } + + @Test + fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val capabilities = mock().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(mock()) + } + + getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + + job.cancel() + } + + @Test + fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + // Start with the original network + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + + // WHEN we update to a new primary network + val newNetworkId = 456 + val newNetwork = mock().apply { + whenever(this.getNetId()).thenReturn(newNetworkId) + } + val newSsid = "CD" + val newWifiInfo = mock().apply { + whenever(this.ssid).thenReturn(newSsid) + whenever(this.isPrimary).thenReturn(true) + } + + getNetworkCallback().onCapabilitiesChanged( + newNetwork, createWifiNetworkCapabilities(newWifiInfo) + ) + + // THEN we use the new network + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.networkId).isEqualTo(newNetworkId) + assertThat(latestActive.ssid).isEqualTo(newSsid) + + job.cancel() + } + + @Test + fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + // Start with the original network + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + + // WHEN we notify of a new but non-primary network + val newNetworkId = 456 + val newNetwork = mock().apply { + whenever(this.getNetId()).thenReturn(newNetworkId) + } + val newSsid = "EF" + val newWifiInfo = mock().apply { + whenever(this.ssid).thenReturn(newSsid) + whenever(this.isPrimary).thenReturn(false) + } + + getNetworkCallback().onCapabilitiesChanged( + newNetwork, createWifiNetworkCapabilities(newWifiInfo) + ) + + // THEN we still use the original network + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) + assertThat(latestActive.ssid).isEqualTo(SSID) + + job.cancel() + } + + @Test + fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock().apply { + whenever(this.ssid).thenReturn(SSID) + whenever(this.isPrimary).thenReturn(true) + } + + // Start with the original network + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + // WHEN we keep the same network ID but change the SSID + val newSsid = "CD" + val newWifiInfo = mock().apply { + whenever(this.ssid).thenReturn(newSsid) + whenever(this.isPrimary).thenReturn(true) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(newWifiInfo)) + + // THEN we've updated to the new SSID + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) + assertThat(latestActive.ssid).isEqualTo(newSsid) + + job.cancel() + } + + @Test + fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand + getNetworkCallback().onLost(NETWORK) + + // THEN there's no crash and we still have no network + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + + job.cancel() + } + + @Test + fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID) + + // WHEN we lose our current network + getNetworkCallback().onLost(NETWORK) + + // THEN we update to no network + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + + job.cancel() + } + + @Test + fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID) + + // WHEN we lose an unknown network + val unknownNetwork = mock().apply { + whenever(this.getNetId()).thenReturn(543) + } + getNetworkCallback().onLost(unknownNetwork) + + // THEN we still have our previous network + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) + assertThat(latestActive.ssid).isEqualTo(SSID) + + job.cancel() + } + + @Test + fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID) + + // WHEN we update to a new network... + val newNetworkId = 89 + val newNetwork = mock().apply { + whenever(this.getNetId()).thenReturn(newNetworkId) + } + getNetworkCallback().onCapabilitiesChanged( + newNetwork, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO) + ) + // ...and lose the old network + getNetworkCallback().onLost(NETWORK) + + // THEN we still have the new network + assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId) + + job.cancel() } @Test fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) { underTest = WifiRepositoryImpl( + connectivityManager, wifiManager = null, executor, logger, @@ -77,12 +425,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) { - underTest = WifiRepositoryImpl( - wifiManager, - executor, - logger, - ) - var latest: WifiActivityModel? = null val job = underTest .wifiActivity @@ -100,12 +442,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) { - underTest = WifiRepositoryImpl( - wifiManager, - executor, - logger, - ) - var latest: WifiActivityModel? = null val job = underTest .wifiActivity @@ -123,12 +459,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) { - underTest = WifiRepositoryImpl( - wifiManager, - executor, - logger, - ) - var latest: WifiActivityModel? = null val job = underTest .wifiActivity @@ -146,12 +476,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) { - underTest = WifiRepositoryImpl( - wifiManager, - executor, - logger, - ) - var latest: WifiActivityModel? = null val job = underTest .wifiActivity @@ -170,6 +494,30 @@ class WifiRepositoryImplTest : SysuiTestCase() { verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture()) return callbackCaptor.value!! } + + private fun getNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor() + verify(connectivityManager).registerNetworkCallback(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun createWifiNetworkCapabilities(wifiInfo: WifiInfo) = + mock().apply { + whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(this.transportInfo).thenReturn(wifiInfo) + } + + private companion object { + const val NETWORK_ID = 45 + val NETWORK = mock().apply { + whenever(this.getNetId()).thenReturn(NETWORK_ID) + } + const val SSID = "AB" + val PRIMARY_WIFI_INFO: WifiInfo = mock().apply { + whenever(this.ssid).thenReturn(SSID) + whenever(this.isPrimary).thenReturn(true) + } + } } private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt index c52f347a605ae..5f1b1dbb19dcd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel +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.Dispatchers @@ -47,7 +47,7 @@ class WifiInteractorTest : SysuiTestCase() { @Test fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) { - repository.setWifiModel(WifiModel(ssid = "AB")) + repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false)) var latest: Boolean? = null @@ -63,7 +63,7 @@ class WifiInteractorTest : SysuiTestCase() { @Test fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) { - repository.setWifiModel(WifiModel(ssid = "AB")) + repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true)) var latest: Boolean? = null @@ -79,7 +79,7 @@ class WifiInteractorTest : SysuiTestCase() { @Test fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) { - repository.setWifiModel(WifiModel(ssid = "AB")) + repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false)) var latest: Boolean? = null @@ -95,7 +95,7 @@ class WifiInteractorTest : SysuiTestCase() { @Test fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) { - repository.setWifiModel(WifiModel(ssid = "AB")) + repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true)) var latest: Boolean? = null @@ -111,7 +111,7 @@ class WifiInteractorTest : SysuiTestCase() { @Test fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) { - repository.setWifiModel(WifiModel(ssid = null)) + repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null)) repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true)) var latest: Boolean? = null @@ -127,7 +127,7 @@ class WifiInteractorTest : SysuiTestCase() { @Test fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) { - repository.setWifiModel(WifiModel(ssid = "AB")) + repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) var latest: Boolean? = null val job = underTest @@ -158,6 +158,10 @@ class WifiInteractorTest : SysuiTestCase() { job.cancel() } + + companion object { + val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB") + } } private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index e9259b071e0b9..fae4b10559682 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants @@ -61,7 +61,7 @@ class WifiViewModelTest : SysuiTestCase() { ) // Set up with a valid SSID - repository.setWifiModel(WifiModel(ssid = "AB")) + repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = "AB")) } @Test