1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod 18 19 import android.annotation.SuppressLint 20 import android.content.IntentFilter 21 import android.net.ConnectivityManager 22 import android.net.Network 23 import android.net.NetworkCapabilities 24 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN 25 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED 26 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR 27 import android.net.NetworkCapabilities.TRANSPORT_WIFI 28 import android.net.NetworkRequest 29 import android.net.wifi.WifiInfo 30 import android.net.wifi.WifiManager 31 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID 32 import com.android.systemui.broadcast.BroadcastDispatcher 33 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 34 import com.android.systemui.dagger.SysUISingleton 35 import com.android.systemui.dagger.qualifiers.Application 36 import com.android.systemui.dagger.qualifiers.Background 37 import com.android.systemui.dagger.qualifiers.Main 38 import com.android.systemui.log.table.TableLogBuffer 39 import com.android.systemui.log.table.logDiffsForTable 40 import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog 41 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel 42 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository 43 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.getMainOrUnderlyingWifiInfo 44 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository 45 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.CARRIER_MERGED_INVALID_SUB_ID_REASON 46 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT 47 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED 48 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger 49 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger 50 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel 51 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry 52 import java.util.concurrent.Executor 53 import javax.inject.Inject 54 import kotlinx.coroutines.CoroutineDispatcher 55 import kotlinx.coroutines.CoroutineScope 56 import kotlinx.coroutines.ExperimentalCoroutinesApi 57 import kotlinx.coroutines.channels.awaitClose 58 import kotlinx.coroutines.flow.Flow 59 import kotlinx.coroutines.flow.MutableSharedFlow 60 import kotlinx.coroutines.flow.MutableStateFlow 61 import kotlinx.coroutines.flow.SharingStarted 62 import kotlinx.coroutines.flow.StateFlow 63 import kotlinx.coroutines.flow.asStateFlow 64 import kotlinx.coroutines.flow.distinctUntilChanged 65 import kotlinx.coroutines.flow.map 66 import kotlinx.coroutines.flow.mapLatest 67 import kotlinx.coroutines.flow.merge 68 import kotlinx.coroutines.flow.onEach 69 import kotlinx.coroutines.flow.onStart 70 import kotlinx.coroutines.flow.stateIn 71 import kotlinx.coroutines.launch 72 import kotlinx.coroutines.withContext 73 74 /** Real implementation of [WifiRepository]. */ 75 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") 76 @OptIn(ExperimentalCoroutinesApi::class) 77 @SysUISingleton 78 @SuppressLint("MissingPermission") 79 class WifiRepositoryImpl 80 @Inject 81 constructor( 82 broadcastDispatcher: BroadcastDispatcher, 83 connectivityManager: ConnectivityManager, 84 connectivityRepository: ConnectivityRepository, 85 logger: WifiInputLogger, 86 @WifiTableLog wifiTableLogBuffer: TableLogBuffer, 87 @Main mainExecutor: Executor, 88 @Background private val bgDispatcher: CoroutineDispatcher, 89 @Application private val scope: CoroutineScope, 90 private val wifiManager: WifiManager, 91 ) : WifiRepositoryDagger { 92 93 override fun start() { 94 // There are two possible [WifiRepository] implementations: This class (old) and 95 // [WifiRepositoryFromTrackerLib] (new). While we migrate to the new class, we want this old 96 // class to still be running in the background so that we can collect logs and compare 97 // discrepancies. This #start method collects on the flows to ensure that the logs are 98 // collected. 99 scope.launch { isWifiEnabled.collect {} } 100 scope.launch { isWifiDefault.collect {} } 101 scope.launch { wifiNetwork.collect {} } 102 scope.launch { wifiActivity.collect {} } 103 } 104 105 private val wifiStateChangeEvents: Flow<Unit> = 106 broadcastDispatcher 107 .broadcastFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)) 108 .onEach { logger.logIntent("WIFI_STATE_CHANGED_ACTION") } 109 110 private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> = 111 MutableSharedFlow(extraBufferCapacity = 1) 112 113 // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it 114 // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may 115 // have changed. 116 override val isWifiEnabled: StateFlow<Boolean> = 117 merge(wifiNetworkChangeEvents, wifiStateChangeEvents) 118 .onStart { emit(Unit) } 119 .mapLatest { isWifiEnabled() } 120 .distinctUntilChanged() 121 .logDiffsForTable( 122 wifiTableLogBuffer, 123 columnPrefix = "", 124 columnName = COL_NAME_IS_ENABLED, 125 initialValue = false, 126 ) 127 .stateIn( 128 scope = scope, 129 started = SharingStarted.Eagerly, 130 initialValue = false, 131 ) 132 133 // [WifiManager.isWifiEnabled] is a blocking IPC call, so fetch it in the background. 134 private suspend fun isWifiEnabled(): Boolean = 135 withContext(bgDispatcher) { wifiManager.isWifiEnabled } 136 137 override val isWifiDefault: StateFlow<Boolean> = 138 connectivityRepository.defaultConnections 139 // TODO(b/274493701): Should wifi be considered default if it's carrier merged? 140 .map { it.wifi.isDefault || it.carrierMerged.isDefault } 141 .distinctUntilChanged() 142 .logDiffsForTable( 143 wifiTableLogBuffer, 144 columnPrefix = "", 145 columnName = COL_NAME_IS_DEFAULT, 146 initialValue = false, 147 ) 148 .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) 149 150 override val wifiNetwork: StateFlow<WifiNetworkModel> = 151 conflatedCallbackFlow { 152 var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT 153 154 val callback = 155 object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { 156 override fun onCapabilitiesChanged( 157 network: Network, 158 networkCapabilities: NetworkCapabilities 159 ) { 160 logger.logOnCapabilitiesChanged( 161 network, 162 networkCapabilities, 163 isDefaultNetworkCallback = false, 164 ) 165 166 wifiNetworkChangeEvents.tryEmit(Unit) 167 168 val wifiInfo = 169 networkCapabilities.getMainOrUnderlyingWifiInfo(connectivityManager) 170 if (wifiInfo?.isPrimary == true) { 171 val wifiNetworkModel = 172 createWifiNetworkModel( 173 wifiInfo, 174 network, 175 networkCapabilities, 176 wifiManager, 177 ) 178 currentWifi = wifiNetworkModel 179 trySend(wifiNetworkModel) 180 } 181 } 182 183 override fun onLost(network: Network) { 184 logger.logOnLost(network, isDefaultNetworkCallback = false) 185 186 wifiNetworkChangeEvents.tryEmit(Unit) 187 188 val wifi = currentWifi 189 if ( 190 (wifi is WifiNetworkModel.Active && 191 wifi.networkId == network.getNetId()) || 192 (wifi is WifiNetworkModel.CarrierMerged && 193 wifi.networkId == network.getNetId()) 194 ) { 195 val newNetworkModel = WifiNetworkModel.Inactive 196 currentWifi = newNetworkModel 197 trySend(newNetworkModel) 198 } 199 } 200 } 201 202 connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback) 203 204 awaitClose { connectivityManager.unregisterNetworkCallback(callback) } 205 } 206 .distinctUntilChanged() 207 .logDiffsForTable( 208 wifiTableLogBuffer, 209 columnPrefix = "", 210 initialValue = WIFI_NETWORK_DEFAULT, 211 ) 212 // There will be multiple wifi icons in different places that will frequently 213 // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures 214 // that new subscribes will get the latest value immediately upon subscription. 215 // Otherwise, the views could show stale data. See b/244173280. 216 .stateIn( 217 scope, 218 started = SharingStarted.WhileSubscribed(), 219 initialValue = WIFI_NETWORK_DEFAULT, 220 ) 221 222 // Secondary networks can only be supported by [WifiRepositoryViaTrackerLib]. 223 override val secondaryNetworks: StateFlow<List<WifiNetworkModel>> = 224 MutableStateFlow(emptyList<WifiNetworkModel>()).asStateFlow() 225 226 override val wifiActivity: StateFlow<DataActivityModel> = 227 WifiRepositoryHelper.createActivityFlow( 228 wifiManager, 229 mainExecutor, 230 scope, 231 wifiTableLogBuffer, 232 logger::logActivity, 233 ) 234 235 override val wifiScanResults: StateFlow<List<WifiScanEntry>> = 236 WifiRepositoryHelper.createNetworkScanFlow( 237 wifiManager, 238 scope, 239 bgDispatcher, 240 logger::logScanResults 241 ) 242 243 companion object { 244 // Start out with no known wifi network. 245 // Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an 246 // initial fetch to get a starting wifi network. But, it uses a deprecated API 247 // [WifiManager.getConnectionInfo()], and the deprecation doc indicates to just use 248 // [ConnectivityManager.NetworkCallback] results instead. So, for now we'll just rely on the 249 // NetworkCallback inside [wifiNetwork] for our wifi network information. 250 val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive 251 252 const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED 253 254 private fun createWifiNetworkModel( 255 wifiInfo: WifiInfo, 256 network: Network, 257 networkCapabilities: NetworkCapabilities, 258 wifiManager: WifiManager, 259 ): WifiNetworkModel { 260 return if (wifiInfo.isCarrierMerged) { 261 if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) { 262 WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON) 263 } else { 264 WifiNetworkModel.CarrierMerged( 265 networkId = network.getNetId(), 266 subscriptionId = wifiInfo.subscriptionId, 267 level = wifiManager.calculateSignalLevel(wifiInfo.rssi), 268 // The WiFi signal level returned by WifiManager#calculateSignalLevel start 269 // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level 270 // buckets count. 271 numberOfLevels = wifiManager.maxSignalLevel + 1, 272 ) 273 } 274 } else { 275 WifiNetworkModel.Active( 276 network.getNetId(), 277 isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED), 278 level = wifiManager.calculateSignalLevel(wifiInfo.rssi), 279 wifiInfo.ssid, 280 // This repository doesn't support any hotspot information. 281 WifiNetworkModel.HotspotDeviceType.NONE, 282 wifiInfo.isPasspointAp, 283 wifiInfo.isOsuAp, 284 wifiInfo.passpointProviderFriendlyName 285 ) 286 } 287 } 288 289 private val WIFI_NETWORK_CALLBACK_REQUEST: NetworkRequest = 290 NetworkRequest.Builder() 291 .clearCapabilities() 292 .addCapability(NET_CAPABILITY_NOT_VPN) 293 .addTransportType(TRANSPORT_WIFI) 294 .addTransportType(TRANSPORT_CELLULAR) 295 .build() 296 } 297 298 @SysUISingleton 299 class Factory 300 @Inject 301 constructor( 302 private val broadcastDispatcher: BroadcastDispatcher, 303 private val connectivityManager: ConnectivityManager, 304 private val connectivityRepository: ConnectivityRepository, 305 private val logger: WifiInputLogger, 306 @WifiTableLog private val wifiTableLogBuffer: TableLogBuffer, 307 @Main private val mainExecutor: Executor, 308 @Background private val bgDispatcher: CoroutineDispatcher, 309 @Application private val scope: CoroutineScope, 310 ) { 311 fun create(wifiManager: WifiManager): WifiRepositoryImpl { 312 return WifiRepositoryImpl( 313 broadcastDispatcher, 314 connectivityManager, 315 connectivityRepository, 316 logger, 317 wifiTableLogBuffer, 318 mainExecutor, 319 bgDispatcher, 320 scope, 321 wifiManager, 322 ) 323 } 324 } 325 } 326