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