blob: c47c20d280c7d76019ea2a645c3c3fa9e433db7b [file] [log] [blame]
/*
* 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.prod
import android.annotation.SuppressLint
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
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.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
import com.android.settingslib.Utils
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
/** Real implementation of [WifiRepository]. */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
@SuppressLint("MissingPermission")
class WifiRepositoryImpl
@Inject
constructor(
broadcastDispatcher: BroadcastDispatcher,
connectivityManager: ConnectivityManager,
logger: ConnectivityPipelineLogger,
@WifiTableLog wifiTableLogBuffer: TableLogBuffer,
@Main mainExecutor: Executor,
@Application scope: CoroutineScope,
wifiManager: WifiManager,
) : RealWifiRepository {
private val wifiStateChangeEvents: Flow<Unit> =
broadcastDispatcher
.broadcastFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION))
.logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent")
private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> =
MutableSharedFlow(extraBufferCapacity = 1)
// Because [WifiManager] doesn't expose a wifi enabled change listener, we do it
// internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may
// have changed.
override val isWifiEnabled: StateFlow<Boolean> =
merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
.mapLatest { wifiManager.isWifiEnabled }
.distinctUntilChanged()
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "",
columnName = "isWifiEnabled",
initialValue = wifiManager.isWifiEnabled,
)
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
initialValue = wifiManager.isWifiEnabled,
)
override val isWifiDefault: StateFlow<Boolean> =
conflatedCallbackFlow {
// Note: This callback doesn't do any logging because we already log every network
// change in the [wifiNetwork] callback.
val callback =
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
// This method will always be called immediately after the network
// becomes the default, in addition to any time the capabilities change
// while the network is the default.
// If this network contains valid wifi info, then wifi is the default
// network.
val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
trySend(wifiInfo != null)
}
override fun onLost(network: Network) {
// The system no longer has a default network, so wifi is definitely not
// default.
trySend(false)
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
.distinctUntilChanged()
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "",
columnName = "isWifiDefault",
initialValue = false,
)
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
override val wifiNetwork: StateFlow<WifiNetworkModel> =
conflatedCallbackFlow {
var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
val callback =
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
logger.logOnCapabilitiesChanged(network, networkCapabilities)
wifiNetworkChangeEvents.tryEmit(Unit)
val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
if (wifiInfo?.isPrimary == true) {
val wifiNetworkModel =
createWifiNetworkModel(
wifiInfo,
network,
networkCapabilities,
wifiManager,
)
logger.logTransformation(
WIFI_NETWORK_CALLBACK_NAME,
oldValue = currentWifi,
newValue = wifiNetworkModel,
)
currentWifi = wifiNetworkModel
trySend(wifiNetworkModel)
}
}
override fun onLost(network: Network) {
logger.logOnLost(network)
wifiNetworkChangeEvents.tryEmit(Unit)
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)
}
}
}
connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
.distinctUntilChanged()
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "wifiNetwork",
initialValue = WIFI_NETWORK_DEFAULT,
)
// There will be multiple wifi icons in different places that will frequently
// subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures
// that new subscribes will get the latest value immediately upon subscription.
// Otherwise, the views could show stale data. See b/244173280.
.stateIn(
scope,
started = SharingStarted.WhileSubscribed(),
initialValue = WIFI_NETWORK_DEFAULT,
)
override val wifiActivity: StateFlow<DataActivityModel> =
conflatedCallbackFlow {
val callback = TrafficStateCallback { state ->
logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
trySend(state.toWifiDataActivityModel())
}
wifiManager.registerTrafficStateCallback(mainExecutor, callback)
awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
}
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = ACTIVITY_PREFIX,
initialValue = ACTIVITY_DEFAULT,
)
.stateIn(
scope,
started = SharingStarted.WhileSubscribed(),
initialValue = ACTIVITY_DEFAULT,
)
companion object {
private const val ACTIVITY_PREFIX = "wifiActivity"
val ACTIVITY_DEFAULT = DataActivityModel(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 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 createWifiNetworkModel(
wifiInfo: WifiInfo,
network: Network,
networkCapabilities: NetworkCapabilities,
wifiManager: WifiManager,
): WifiNetworkModel {
return if (wifiInfo.isCarrierMerged) {
WifiNetworkModel.CarrierMerged
} else {
WifiNetworkModel.Active(
network.getNetId(),
isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED),
level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
wifiInfo.ssid,
wifiInfo.isPasspointAp,
wifiInfo.isOsuAp,
wifiInfo.passpointProviderFriendlyName
)
}
}
private fun prettyPrintActivity(activity: Int): String {
return when (activity) {
TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE"
TrafficStateCallback.DATA_ACTIVITY_IN -> "IN"
TrafficStateCallback.DATA_ACTIVITY_OUT -> "OUT"
TrafficStateCallback.DATA_ACTIVITY_INOUT -> "INOUT"
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"
}
@SysUISingleton
class Factory
@Inject
constructor(
private val broadcastDispatcher: BroadcastDispatcher,
private val connectivityManager: ConnectivityManager,
private val logger: ConnectivityPipelineLogger,
@WifiTableLog private val wifiTableLogBuffer: TableLogBuffer,
@Main private val mainExecutor: Executor,
@Application private val scope: CoroutineScope,
) {
fun create(wifiManager: WifiManager): WifiRepositoryImpl {
return WifiRepositoryImpl(
broadcastDispatcher,
connectivityManager,
logger,
wifiTableLogBuffer,
mainExecutor,
scope,
wifiManager,
)
}
}
}