• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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