1 /* <lambda>null2 * Copyright (C) 2023 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.net.wifi.ScanResult 21 import android.net.wifi.WifiManager 22 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID 23 import androidx.lifecycle.Lifecycle 24 import androidx.lifecycle.LifecycleOwner 25 import androidx.lifecycle.LifecycleRegistry 26 import com.android.internal.annotations.VisibleForTesting 27 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.dagger.qualifiers.Background 31 import com.android.systemui.dagger.qualifiers.Main 32 import com.android.systemui.flags.FeatureFlags 33 import com.android.systemui.flags.Flags 34 import com.android.systemui.log.LogBuffer 35 import com.android.systemui.log.core.LogLevel 36 import com.android.systemui.log.table.TableLogBuffer 37 import com.android.systemui.log.table.logDiffsForTable 38 import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory 39 import com.android.systemui.statusbar.pipeline.dagger.WifiInputLog 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.model.toWifiDataActivityModel 43 import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository 44 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.CARRIER_MERGED_INVALID_SUB_ID_REASON 45 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT 46 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED 47 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel 48 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Inactive.toHotspotDeviceType 49 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry 50 import com.android.wifitrackerlib.HotspotNetworkEntry 51 import com.android.wifitrackerlib.MergedCarrierEntry 52 import com.android.wifitrackerlib.WifiEntry 53 import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MAX 54 import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MIN 55 import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE 56 import com.android.wifitrackerlib.WifiPickerTracker 57 import java.util.concurrent.Executor 58 import javax.inject.Inject 59 import kotlinx.coroutines.CoroutineDispatcher 60 import kotlinx.coroutines.CoroutineScope 61 import kotlinx.coroutines.asExecutor 62 import kotlinx.coroutines.channels.awaitClose 63 import kotlinx.coroutines.flow.SharingStarted 64 import kotlinx.coroutines.flow.StateFlow 65 import kotlinx.coroutines.flow.callbackFlow 66 import kotlinx.coroutines.flow.distinctUntilChanged 67 import kotlinx.coroutines.flow.map 68 import kotlinx.coroutines.flow.stateIn 69 70 /** 71 * A real implementation of [WifiRepository] that uses [com.android.wifitrackerlib] as the source of 72 * truth for wifi information. 73 */ 74 @SysUISingleton 75 class WifiRepositoryImpl 76 @Inject 77 constructor( 78 featureFlags: FeatureFlags, 79 @Application private val scope: CoroutineScope, 80 @Main private val mainExecutor: Executor, 81 @Background private val bgDispatcher: CoroutineDispatcher, 82 private val wifiPickerTrackerFactory: WifiPickerTrackerFactory, 83 private val wifiManager: WifiManager, 84 @WifiInputLog private val inputLogger: LogBuffer, 85 @WifiTableLog private val tableLogger: TableLogBuffer, 86 ) : RealWifiRepository, LifecycleOwner { 87 88 override val lifecycle = 89 LifecycleRegistry(this).also { 90 mainExecutor.execute { it.currentState = Lifecycle.State.CREATED } 91 } 92 93 private val isInstantTetherEnabled = featureFlags.isEnabled(Flags.INSTANT_TETHER) 94 95 private var wifiPickerTracker: WifiPickerTracker? = null 96 97 private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> = run { 98 var current = 99 WifiPickerTrackerInfo( 100 state = WIFI_STATE_DEFAULT, 101 isDefault = false, 102 primaryNetwork = WIFI_NETWORK_DEFAULT, 103 secondaryNetworks = emptyList(), 104 ) 105 callbackFlow { 106 val callback = 107 object : WifiPickerTracker.WifiPickerTrackerCallback { 108 override fun onWifiEntriesChanged() { 109 val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection 110 logOnWifiEntriesChanged(connectedEntry) 111 112 val secondaryNetworks = 113 if (featureFlags.isEnabled(Flags.WIFI_SECONDARY_NETWORKS)) { 114 val activeNetworks = 115 wifiPickerTracker?.activeWifiEntries ?: emptyList() 116 activeNetworks 117 .filter { it != connectedEntry && !it.isPrimaryNetwork } 118 .map { it.toWifiNetworkModel() } 119 } else { 120 emptyList() 121 } 122 123 // [WifiPickerTracker.connectedWifiEntry] will return the same instance 124 // but with updated internals. For example, when its validation status 125 // changes from false to true, the same instance is re-used but with the 126 // validated field updated. 127 // 128 // Because it's the same instance, the flow won't re-emit the value 129 // (even though the internals have changed). So, we need to transform it 130 // into our internal model immediately. [toWifiNetworkModel] always 131 // returns a new instance, so the flow is guaranteed to emit. 132 send( 133 newPrimaryNetwork = connectedEntry?.toPrimaryWifiNetworkModel() 134 ?: WIFI_NETWORK_DEFAULT, 135 newSecondaryNetworks = secondaryNetworks, 136 newIsDefault = connectedEntry?.isDefaultNetwork ?: false, 137 ) 138 } 139 140 override fun onWifiStateChanged() { 141 val state = wifiPickerTracker?.wifiState 142 logOnWifiStateChanged(state) 143 send(newState = state ?: WIFI_STATE_DEFAULT) 144 } 145 146 override fun onNumSavedNetworksChanged() {} 147 148 override fun onNumSavedSubscriptionsChanged() {} 149 150 private fun send( 151 newState: Int = current.state, 152 newIsDefault: Boolean = current.isDefault, 153 newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork, 154 newSecondaryNetworks: List<WifiNetworkModel> = 155 current.secondaryNetworks, 156 ) { 157 val new = 158 WifiPickerTrackerInfo( 159 newState, 160 newIsDefault, 161 newPrimaryNetwork, 162 newSecondaryNetworks, 163 ) 164 current = new 165 trySend(new) 166 } 167 } 168 169 wifiPickerTracker = 170 wifiPickerTrackerFactory.create(lifecycle, callback, "WifiRepository").apply { 171 // By default, [WifiPickerTracker] will scan to see all available wifi 172 // networks in the area. Because SysUI only needs to display the 173 // **connected** network, we don't need scans to be running (and in fact, 174 // running scans is costly and should be avoided whenever possible). 175 this?.disableScanning() 176 } 177 // The lifecycle must be STARTED in order for the callback to receive events. 178 mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED } 179 awaitClose { 180 mainExecutor.execute { lifecycle.currentState = Lifecycle.State.CREATED } 181 } 182 } 183 .stateIn(scope, SharingStarted.Eagerly, current) 184 } 185 186 override val isWifiEnabled: StateFlow<Boolean> = 187 wifiPickerTrackerInfo 188 .map { it.state == WifiManager.WIFI_STATE_ENABLED } 189 .distinctUntilChanged() 190 .logDiffsForTable( 191 tableLogger, 192 columnPrefix = "", 193 columnName = COL_NAME_IS_ENABLED, 194 initialValue = false, 195 ) 196 .stateIn(scope, SharingStarted.Eagerly, false) 197 198 override val wifiNetwork: StateFlow<WifiNetworkModel> = 199 wifiPickerTrackerInfo 200 .map { it.primaryNetwork } 201 .distinctUntilChanged() 202 .logDiffsForTable( 203 tableLogger, 204 columnPrefix = "", 205 initialValue = WIFI_NETWORK_DEFAULT, 206 ) 207 .stateIn(scope, SharingStarted.Eagerly, WIFI_NETWORK_DEFAULT) 208 209 override val secondaryNetworks: StateFlow<List<WifiNetworkModel>> = 210 wifiPickerTrackerInfo 211 .map { it.secondaryNetworks } 212 .distinctUntilChanged() 213 .logDiffsForTable( 214 tableLogger, 215 columnPrefix = "", 216 columnName = "secondaryNetworks", 217 initialValue = emptyList(), 218 ) 219 .stateIn(scope, SharingStarted.Eagerly, emptyList()) 220 221 /** 222 * [WifiPickerTracker.getConnectedWifiEntry] stores a [MergedCarrierEntry] separately from the 223 * [WifiEntry] for the primary connection. Therefore, we have to prefer the carrier-merged entry 224 * if it exists, falling back on the connected entry if null 225 */ 226 private val WifiPickerTracker?.mergedOrPrimaryConnection: WifiEntry? 227 get() { 228 val mergedEntry: MergedCarrierEntry? = this?.mergedCarrierEntry 229 return if (mergedEntry != null && mergedEntry.isDefaultNetwork) { 230 mergedEntry 231 } else { 232 this?.connectedWifiEntry 233 } 234 } 235 236 /** 237 * Converts WifiTrackerLib's [WifiEntry] into our internal model only if the entry is the 238 * primary network. Returns an inactive network if it's not primary. 239 */ 240 private fun WifiEntry.toPrimaryWifiNetworkModel(): WifiNetworkModel { 241 return if (!this.isPrimaryNetwork) { 242 WIFI_NETWORK_DEFAULT 243 } else { 244 this.toWifiNetworkModel() 245 } 246 } 247 248 /** Converts WifiTrackerLib's [WifiEntry] into our internal model. */ 249 private fun WifiEntry.toWifiNetworkModel(): WifiNetworkModel { 250 return if (this is MergedCarrierEntry) { 251 this.convertCarrierMergedToModel() 252 } else { 253 this.convertNormalToModel() 254 } 255 } 256 257 private fun MergedCarrierEntry.convertCarrierMergedToModel(): WifiNetworkModel { 258 return if (this.subscriptionId == INVALID_SUBSCRIPTION_ID) { 259 WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON) 260 } else { 261 WifiNetworkModel.CarrierMerged( 262 networkId = NETWORK_ID, 263 subscriptionId = this.subscriptionId, 264 level = this.level, 265 // WifiManager APIs to calculate the signal level start from 0, so 266 // maxSignalLevel + 1 represents the total level buckets count. 267 numberOfLevels = wifiManager.maxSignalLevel + 1, 268 ) 269 } 270 } 271 272 private fun WifiEntry.convertNormalToModel(): WifiNetworkModel { 273 if (this.level == WIFI_LEVEL_UNREACHABLE || this.level !in WIFI_LEVEL_MIN..WIFI_LEVEL_MAX) { 274 // If our level means the network is unreachable or the level is otherwise invalid, we 275 // don't have an active network. 276 return WifiNetworkModel.Inactive 277 } 278 279 val hotspotDeviceType = 280 if (isInstantTetherEnabled && this is HotspotNetworkEntry) { 281 this.deviceType.toHotspotDeviceType() 282 } else { 283 WifiNetworkModel.HotspotDeviceType.NONE 284 } 285 286 return WifiNetworkModel.Active( 287 networkId = NETWORK_ID, 288 isValidated = this.hasInternetAccess(), 289 level = this.level, 290 ssid = this.title, 291 hotspotDeviceType = hotspotDeviceType, 292 // With WifiTrackerLib, [WifiEntry.title] will appropriately fetch the SSID for 293 // typical wifi networks *and* passpoint/OSU APs. So, the AP-specific values can 294 // always be false/null in this repository. 295 // TODO(b/292534484): Remove these fields from the wifi network model once this 296 // repository is fully enabled. 297 isPasspointAccessPoint = false, 298 isOnlineSignUpForPasspointAccessPoint = false, 299 passpointProviderFriendlyName = null, 300 ) 301 } 302 303 override val isWifiDefault: StateFlow<Boolean> = 304 wifiPickerTrackerInfo 305 .map { it.isDefault } 306 .distinctUntilChanged() 307 .logDiffsForTable( 308 tableLogger, 309 columnPrefix = "", 310 columnName = COL_NAME_IS_DEFAULT, 311 initialValue = false, 312 ) 313 .stateIn(scope, SharingStarted.Eagerly, false) 314 315 override val wifiActivity: StateFlow<DataActivityModel> = 316 conflatedCallbackFlow { 317 val callback = 318 WifiManager.TrafficStateCallback { state -> 319 logActivity(state) 320 trySend(state.toWifiDataActivityModel()) 321 } 322 wifiManager.registerTrafficStateCallback(mainExecutor, callback) 323 awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } 324 } 325 .stateIn( 326 scope, 327 started = SharingStarted.WhileSubscribed(), 328 initialValue = ACTIVITY_DEFAULT, 329 ) 330 331 override val wifiScanResults: StateFlow<List<WifiScanEntry>> = 332 conflatedCallbackFlow { 333 val callback = 334 object : WifiManager.ScanResultsCallback() { 335 @SuppressLint("MissingPermission") 336 override fun onScanResultsAvailable() { 337 logScanResults() 338 trySend(wifiManager.scanResults.toModel()) 339 } 340 } 341 342 wifiManager.registerScanResultsCallback(bgDispatcher.asExecutor(), callback) 343 344 awaitClose { wifiManager.unregisterScanResultsCallback(callback) } 345 } 346 .stateIn(scope, SharingStarted.Eagerly, emptyList()) 347 348 private fun List<ScanResult>.toModel(): List<WifiScanEntry> = map { WifiScanEntry(it.SSID) } 349 350 private fun logOnWifiEntriesChanged(connectedEntry: WifiEntry?) { 351 inputLogger.log( 352 TAG, 353 LogLevel.DEBUG, 354 { str1 = connectedEntry.toString() }, 355 { "onWifiEntriesChanged. ConnectedEntry=$str1" }, 356 ) 357 } 358 359 private fun logOnWifiStateChanged(state: Int?) { 360 inputLogger.log( 361 TAG, 362 LogLevel.DEBUG, 363 { int1 = state ?: -1 }, 364 { "onWifiStateChanged. State=${if (int1 == -1) null else int1}" }, 365 ) 366 } 367 368 private fun logActivity(activity: Int) { 369 inputLogger.log( 370 TAG, 371 LogLevel.DEBUG, 372 { str1 = prettyPrintActivity(activity) }, 373 { "onActivityChanged: $str1" } 374 ) 375 } 376 377 // TODO(b/292534484): This print should only be done in [MessagePrinter] part of the log buffer. 378 private fun prettyPrintActivity(activity: Int): String { 379 return when (activity) { 380 WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE" 381 WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN -> "IN" 382 WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT -> "OUT" 383 WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT -> "INOUT" 384 else -> "INVALID" 385 } 386 } 387 388 private fun logScanResults() = 389 inputLogger.log(TAG, LogLevel.DEBUG, {}, { "onScanResultsAvailable" }) 390 391 /** 392 * Data class storing all the information fetched from [WifiPickerTracker]. 393 * 394 * Used so that we only register a single callback on [WifiPickerTracker]. 395 */ 396 data class WifiPickerTrackerInfo( 397 /** The current wifi state. See [WifiManager.getWifiState]. */ 398 val state: Int, 399 /** True if wifi is currently the default connection and false otherwise. */ 400 val isDefault: Boolean, 401 /** The currently primary wifi network. */ 402 val primaryNetwork: WifiNetworkModel, 403 /** The current secondary network(s), if any. Specifically excludes the primary network. */ 404 val secondaryNetworks: List<WifiNetworkModel> 405 ) 406 407 @SysUISingleton 408 class Factory 409 @Inject 410 constructor( 411 private val featureFlags: FeatureFlags, 412 @Application private val scope: CoroutineScope, 413 @Main private val mainExecutor: Executor, 414 @Background private val bgDispatcher: CoroutineDispatcher, 415 private val wifiPickerTrackerFactory: WifiPickerTrackerFactory, 416 @WifiInputLog private val inputLogger: LogBuffer, 417 @WifiTableLog private val tableLogger: TableLogBuffer, 418 ) { 419 fun create(wifiManager: WifiManager): WifiRepositoryImpl { 420 return WifiRepositoryImpl( 421 featureFlags, 422 scope, 423 mainExecutor, 424 bgDispatcher, 425 wifiPickerTrackerFactory, 426 wifiManager, 427 inputLogger, 428 tableLogger, 429 ) 430 } 431 } 432 433 companion object { 434 // Start out with no known wifi network. 435 @VisibleForTesting val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive 436 437 private const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED 438 439 val ACTIVITY_DEFAULT = DataActivityModel(hasActivityIn = false, hasActivityOut = false) 440 441 private const val TAG = "WifiTrackerLibInputLog" 442 443 /** 444 * [WifiNetworkModel.Active.networkId] is only used at the repository layer. It's used by 445 * [WifiRepositoryImpl], which tracks the ID in order to correctly apply the framework 446 * callbacks within the repository. 447 * 448 * Since this class does not need to manually apply framework callbacks and since the 449 * network ID is not used beyond the repository, it's safe to use an invalid ID in this 450 * repository. 451 * 452 * The [WifiNetworkModel.Active.networkId] field should be deleted once we've fully migrated 453 * to [WifiRepositoryImpl]. 454 */ 455 private const val NETWORK_ID = -1 456 } 457 } 458