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