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