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.content.Context 21 import android.net.wifi.ScanResult 22 import android.net.wifi.WifiManager 23 import android.os.UserHandle 24 import androidx.lifecycle.Lifecycle 25 import androidx.lifecycle.LifecycleOwner 26 import androidx.lifecycle.LifecycleRegistry 27 import com.android.internal.annotations.VisibleForTesting 28 import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport 29 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.dagger.qualifiers.Background 33 import com.android.systemui.dagger.qualifiers.Main 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.COL_NAME_IS_DEFAULT 45 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED 46 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel 47 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Unavailable.toHotspotDeviceType 48 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry 49 import com.android.systemui.user.data.repository.UserRepository 50 import com.android.wifitrackerlib.HotspotNetworkEntry 51 import com.android.wifitrackerlib.MergedCarrierEntry 52 import com.android.wifitrackerlib.WifiEntry 53 import com.android.wifitrackerlib.WifiPickerTracker 54 import java.util.concurrent.Executor 55 import javax.inject.Inject 56 import kotlinx.coroutines.CoroutineDispatcher 57 import kotlinx.coroutines.CoroutineScope 58 import kotlinx.coroutines.asExecutor 59 import kotlinx.coroutines.channels.awaitClose 60 import kotlinx.coroutines.flow.Flow 61 import kotlinx.coroutines.flow.SharingStarted 62 import kotlinx.coroutines.flow.StateFlow 63 import kotlinx.coroutines.flow.callbackFlow 64 import kotlinx.coroutines.flow.distinctUntilChanged 65 import kotlinx.coroutines.flow.flatMapLatest 66 import kotlinx.coroutines.flow.map 67 import kotlinx.coroutines.flow.stateIn 68 69 /** 70 * A real implementation of [WifiRepository] that uses [com.android.wifitrackerlib] as the source of 71 * truth for wifi information. 72 */ 73 @SysUISingleton 74 class WifiRepositoryImpl 75 @Inject 76 constructor( 77 @Application applicationContext: Context, 78 private val userRepository: UserRepository, 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 if (multiuserWifiPickerTrackerSupport()) { 91 mainExecutor.execute { it.currentState = Lifecycle.State.STARTED } 92 } else { 93 mainExecutor.execute { it.currentState = Lifecycle.State.CREATED } 94 } 95 } 96 97 private var wifiPickerTracker: WifiPickerTracker? = null 98 99 private val selectedUserContext: Flow<Context> = 100 userRepository.selectedUserInfo.map { 101 applicationContext.createContextAsUser(UserHandle.of(it.id), /* flags= */ 0) 102 } 103 104 var current = 105 WifiPickerTrackerInfo( 106 state = WIFI_STATE_DEFAULT, 107 isDefault = false, 108 primaryNetwork = WIFI_NETWORK_DEFAULT, 109 secondaryNetworks = emptyList(), 110 ) 111 112 private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> = 113 if (multiuserWifiPickerTrackerSupport()) { 114 selectedUserContext 115 .flatMapLatest { currentContext 116 -> // flatMapLatest because when selectedUserContext emits a new value, we want 117 // to re-create a whole flow 118 callbackFlow { 119 val callback = 120 object : WifiPickerTracker.WifiPickerTrackerCallback { 121 override fun onWifiEntriesChanged() { 122 val connectedEntry = 123 wifiPickerTracker.mergedOrPrimaryConnection 124 logOnWifiEntriesChanged(connectedEntry) 125 126 val activeNetworks = 127 wifiPickerTracker?.activeWifiEntries ?: emptyList() 128 val secondaryNetworks = 129 activeNetworks 130 .filter { 131 it != connectedEntry && !it.isPrimaryNetwork 132 } 133 .map { it.toWifiNetworkModel() } 134 135 // [WifiPickerTracker.connectedWifiEntry] will return the 136 // same instance but with updated internals. For example, 137 // when its validation status changes from false to true, 138 // the same instance is re-used but with the validated 139 // field updated. 140 // 141 // Because it's the same instance, the flow won't re-emit 142 // the value (even though the internals have changed). So, 143 // we need to transform it into our internal model 144 // immediately. [toWifiNetworkModel] always returns a new 145 // instance, so the flow is guaranteed to emit. 146 send( 147 newPrimaryNetwork = 148 connectedEntry?.toPrimaryWifiNetworkModel() 149 ?: WIFI_NETWORK_DEFAULT, 150 newSecondaryNetworks = secondaryNetworks, 151 newIsDefault = connectedEntry?.isDefaultNetwork ?: false, 152 ) 153 } 154 155 override fun onWifiStateChanged() { 156 val state = wifiPickerTracker?.wifiState 157 logOnWifiStateChanged(state) 158 send(newState = state ?: WIFI_STATE_DEFAULT) 159 } 160 161 override fun onNumSavedNetworksChanged() {} 162 163 override fun onNumSavedSubscriptionsChanged() {} 164 165 private fun send( 166 newState: Int = current.state, 167 newIsDefault: Boolean = current.isDefault, 168 newPrimaryNetwork: WifiNetworkModel = 169 current.primaryNetwork, 170 newSecondaryNetworks: List<WifiNetworkModel> = 171 current.secondaryNetworks, 172 ) { 173 val new = 174 WifiPickerTrackerInfo( 175 newState, 176 newIsDefault, 177 newPrimaryNetwork, 178 newSecondaryNetworks, 179 ) 180 current = new 181 trySend(new) 182 } 183 } 184 185 // If a WifiPicker already exists, call onStop to begin its shutdown 186 // process in preparation for a new one to be created. 187 wifiPickerTracker?.onStop() 188 wifiPickerTracker = 189 wifiPickerTrackerFactory 190 .create(currentContext, lifecycle, callback, "WifiRepository") 191 .apply { 192 // By default, [WifiPickerTracker] will scan to see all 193 // available wifi networks in the area. Because SysUI only 194 // needs to display the **connected** network, we don't 195 // need scans to be running (and in fact, running scans is 196 // costly and should be avoided whenever possible). 197 this?.disableScanning() 198 } 199 awaitClose { mainExecutor.execute { wifiPickerTracker?.onStop() } } 200 } 201 .stateIn(scope, SharingStarted.Eagerly, current) 202 } 203 .stateIn(scope, SharingStarted.Eagerly, current) 204 } else { 205 206 run { 207 var current = 208 WifiPickerTrackerInfo( 209 state = WIFI_STATE_DEFAULT, 210 isDefault = false, 211 primaryNetwork = WIFI_NETWORK_DEFAULT, 212 secondaryNetworks = emptyList(), 213 ) 214 callbackFlow { 215 val callback = 216 object : WifiPickerTracker.WifiPickerTrackerCallback { 217 override fun onWifiEntriesChanged() { 218 val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection 219 logOnWifiEntriesChanged(connectedEntry) 220 221 val activeNetworks = 222 wifiPickerTracker?.activeWifiEntries ?: emptyList() 223 val secondaryNetworks = 224 activeNetworks 225 .filter { it != connectedEntry && !it.isPrimaryNetwork } 226 .map { it.toWifiNetworkModel() } 227 228 // [WifiPickerTracker.connectedWifiEntry] will return the same 229 // instance but with updated internals. For example, when its 230 // validation status changes from false to true, the same 231 // instance is re-used but with the validated field updated. 232 // 233 // Because it's the same instance, the flow won't re-emit the 234 // value (even though the internals have changed). So, we need 235 // to transform it into our internal model immediately. 236 // [toWifiNetworkModel] always returns a new instance, so the 237 // flow is guaranteed to emit. 238 send( 239 newPrimaryNetwork = 240 connectedEntry?.toPrimaryWifiNetworkModel() 241 ?: WIFI_NETWORK_DEFAULT, 242 newSecondaryNetworks = secondaryNetworks, 243 newIsDefault = connectedEntry?.isDefaultNetwork ?: false, 244 ) 245 } 246 247 override fun onWifiStateChanged() { 248 val state = wifiPickerTracker?.wifiState 249 logOnWifiStateChanged(state) 250 send(newState = state ?: WIFI_STATE_DEFAULT) 251 } 252 253 override fun onNumSavedNetworksChanged() {} 254 255 override fun onNumSavedSubscriptionsChanged() {} 256 257 private fun send( 258 newState: Int = current.state, 259 newIsDefault: Boolean = current.isDefault, 260 newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork, 261 newSecondaryNetworks: List<WifiNetworkModel> = 262 current.secondaryNetworks, 263 ) { 264 val new = 265 WifiPickerTrackerInfo( 266 newState, 267 newIsDefault, 268 newPrimaryNetwork, 269 newSecondaryNetworks, 270 ) 271 current = new 272 trySend(new) 273 } 274 } 275 wifiPickerTracker = 276 wifiPickerTrackerFactory 277 .create(applicationContext, lifecycle, callback, "WifiRepository") 278 .apply { 279 // By default, [WifiPickerTracker] will scan to see all 280 // available wifi networks in the area. Because SysUI only 281 // needs to display the **connected** network, we don't 282 // need scans to be running (and in fact, running scans is 283 // costly and should be avoided whenever possible). 284 this?.disableScanning() 285 } 286 // The lifecycle must be STARTED in order for the callback to receive 287 // events. 288 mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED } 289 awaitClose { 290 mainExecutor.execute { 291 lifecycle.currentState = Lifecycle.State.CREATED 292 } 293 } 294 } 295 .stateIn(scope, SharingStarted.Eagerly, current) 296 } 297 } 298 299 override val isWifiEnabled: StateFlow<Boolean> = 300 wifiPickerTrackerInfo 301 .map { it.state == WifiManager.WIFI_STATE_ENABLED } 302 .distinctUntilChanged() 303 .logDiffsForTable(tableLogger, columnName = COL_NAME_IS_ENABLED, initialValue = false) 304 .stateIn(scope, SharingStarted.Eagerly, false) 305 306 override val wifiNetwork: StateFlow<WifiNetworkModel> = 307 wifiPickerTrackerInfo 308 .map { it.primaryNetwork } 309 .distinctUntilChanged() 310 .logDiffsForTable(tableLogger, initialValue = WIFI_NETWORK_DEFAULT) 311 .stateIn(scope, SharingStarted.Eagerly, WIFI_NETWORK_DEFAULT) 312 313 override val secondaryNetworks: StateFlow<List<WifiNetworkModel>> = 314 wifiPickerTrackerInfo 315 .map { it.secondaryNetworks } 316 .distinctUntilChanged() 317 .logDiffsForTable( 318 tableLogger, 319 columnName = "secondaryNetworks", 320 initialValue = emptyList(), 321 ) 322 .stateIn(scope, SharingStarted.Eagerly, emptyList()) 323 324 /** 325 * [WifiPickerTracker.getConnectedWifiEntry] stores a [MergedCarrierEntry] separately from the 326 * [WifiEntry] for the primary connection. Therefore, we have to prefer the carrier-merged entry 327 * if it exists, falling back on the connected entry if null 328 */ 329 private val WifiPickerTracker?.mergedOrPrimaryConnection: WifiEntry? 330 get() { 331 val mergedEntry: MergedCarrierEntry? = this?.mergedCarrierEntry 332 return if (mergedEntry != null && mergedEntry.isDefaultNetwork) { 333 mergedEntry 334 } else { 335 this?.connectedWifiEntry 336 } 337 } 338 339 /** 340 * Converts WifiTrackerLib's [WifiEntry] into our internal model only if the entry is the 341 * primary network. Returns an inactive network if it's not primary. 342 */ 343 private fun WifiEntry.toPrimaryWifiNetworkModel(): WifiNetworkModel { 344 return if (!this.isPrimaryNetwork) { 345 WIFI_NETWORK_DEFAULT 346 } else { 347 this.toWifiNetworkModel() 348 } 349 } 350 351 /** Converts WifiTrackerLib's [WifiEntry] into our internal model. */ 352 private fun WifiEntry.toWifiNetworkModel(): WifiNetworkModel { 353 return if (this is MergedCarrierEntry) { 354 this.convertCarrierMergedToModel() 355 } else { 356 this.convertNormalToModel() 357 } 358 } 359 360 private fun MergedCarrierEntry.convertCarrierMergedToModel(): WifiNetworkModel { 361 // WifiEntry instance values aren't guaranteed to be stable between method calls 362 // because WifiPickerTracker is continuously updating the same object. Save the 363 // level in a local variable so that checking the level validity here guarantees 364 // that the level will still be valid when we create the `WifiNetworkModel.Active` 365 // instance later. Otherwise, the level could be valid here but become invalid 366 // later, and `WifiNetworkModel.Active` will throw an exception. See b/362384551. 367 368 return WifiNetworkModel.CarrierMerged.of( 369 subscriptionId = this.subscriptionId, 370 level = this.level, 371 // WifiManager APIs to calculate the signal level start from 0, so 372 // maxSignalLevel + 1 represents the total level buckets count. 373 numberOfLevels = wifiManager.maxSignalLevel + 1, 374 ) 375 } 376 377 private fun WifiEntry.convertNormalToModel(): WifiNetworkModel { 378 val hotspotDeviceType = 379 if (this is HotspotNetworkEntry) { 380 this.deviceType.toHotspotDeviceType() 381 } else { 382 WifiNetworkModel.HotspotDeviceType.NONE 383 } 384 385 return WifiNetworkModel.Active.of( 386 isValidated = this.hasInternetAccess(), 387 level = this.level, 388 ssid = this.title, 389 hotspotDeviceType = hotspotDeviceType, 390 ) 391 } 392 393 override val isWifiDefault: StateFlow<Boolean> = 394 wifiPickerTrackerInfo 395 .map { it.isDefault } 396 .distinctUntilChanged() 397 .logDiffsForTable(tableLogger, columnName = COL_NAME_IS_DEFAULT, initialValue = false) 398 .stateIn(scope, SharingStarted.Eagerly, false) 399 400 override val wifiActivity: StateFlow<DataActivityModel> = 401 conflatedCallbackFlow { 402 val callback = 403 WifiManager.TrafficStateCallback { state -> 404 logActivity(state) 405 trySend(state.toWifiDataActivityModel()) 406 } 407 wifiManager.registerTrafficStateCallback(mainExecutor, callback) 408 awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } 409 } 410 .stateIn( 411 scope, 412 started = SharingStarted.WhileSubscribed(), 413 initialValue = ACTIVITY_DEFAULT, 414 ) 415 416 override val wifiScanResults: StateFlow<List<WifiScanEntry>> = 417 conflatedCallbackFlow { 418 val callback = 419 object : WifiManager.ScanResultsCallback() { 420 @SuppressLint("MissingPermission") 421 override fun onScanResultsAvailable() { 422 logScanResults() 423 trySend(wifiManager.scanResults.toModel()) 424 } 425 } 426 427 wifiManager.registerScanResultsCallback(bgDispatcher.asExecutor(), callback) 428 429 awaitClose { wifiManager.unregisterScanResultsCallback(callback) } 430 } 431 .stateIn(scope, SharingStarted.Eagerly, emptyList()) 432 433 private fun List<ScanResult>.toModel(): List<WifiScanEntry> = map { WifiScanEntry(it.SSID) } 434 435 private fun logOnWifiEntriesChanged(connectedEntry: WifiEntry?) { 436 inputLogger.log( 437 TAG, 438 LogLevel.DEBUG, 439 { str1 = connectedEntry.toString() }, 440 { "onWifiEntriesChanged. ConnectedEntry=$str1" }, 441 ) 442 } 443 444 private fun logOnWifiStateChanged(state: Int?) { 445 inputLogger.log( 446 TAG, 447 LogLevel.DEBUG, 448 { int1 = state ?: -1 }, 449 { "onWifiStateChanged. State=${if (int1 == -1) null else int1}" }, 450 ) 451 } 452 453 private fun logActivity(activity: Int) { 454 inputLogger.log( 455 TAG, 456 LogLevel.DEBUG, 457 { str1 = prettyPrintActivity(activity) }, 458 { "onActivityChanged: $str1" }, 459 ) 460 } 461 462 // TODO(b/292534484): This print should only be done in [MessagePrinter] part of the log buffer. 463 private fun prettyPrintActivity(activity: Int): String { 464 return when (activity) { 465 WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE" 466 WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN -> "IN" 467 WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT -> "OUT" 468 WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT -> "INOUT" 469 else -> "INVALID" 470 } 471 } 472 473 private fun logScanResults() = 474 inputLogger.log(TAG, LogLevel.DEBUG, {}, { "onScanResultsAvailable" }) 475 476 /** 477 * Data class storing all the information fetched from [WifiPickerTracker]. 478 * 479 * Used so that we only register a single callback on [WifiPickerTracker]. 480 */ 481 data class WifiPickerTrackerInfo( 482 /** The current wifi state. See [WifiManager.getWifiState]. */ 483 val state: Int, 484 /** True if wifi is currently the default connection and false otherwise. */ 485 val isDefault: Boolean, 486 /** The currently primary wifi network. */ 487 val primaryNetwork: WifiNetworkModel, 488 /** The current secondary network(s), if any. Specifically excludes the primary network. */ 489 val secondaryNetworks: List<WifiNetworkModel>, 490 ) 491 492 @SysUISingleton 493 class Factory 494 @Inject 495 constructor( 496 @Application private val applicationContext: Context, 497 private val userRepository: UserRepository, 498 @Application private val scope: CoroutineScope, 499 @Main private val mainExecutor: Executor, 500 @Background private val bgDispatcher: CoroutineDispatcher, 501 private val wifiPickerTrackerFactory: WifiPickerTrackerFactory, 502 @WifiInputLog private val inputLogger: LogBuffer, 503 @WifiTableLog private val tableLogger: TableLogBuffer, 504 ) { 505 fun create(wifiManager: WifiManager): WifiRepositoryImpl { 506 return WifiRepositoryImpl( 507 applicationContext, 508 userRepository, 509 scope, 510 mainExecutor, 511 bgDispatcher, 512 wifiPickerTrackerFactory, 513 wifiManager, 514 inputLogger, 515 tableLogger, 516 ) 517 } 518 } 519 520 companion object { 521 // Start out with no known wifi network. 522 @VisibleForTesting val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive() 523 524 private const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED 525 526 val ACTIVITY_DEFAULT = DataActivityModel(hasActivityIn = false, hasActivityOut = false) 527 528 private const val TAG = "WifiTrackerLibInputLog" 529 } 530 } 531