1 /*
2  * Copyright (C) 2022 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.IntentFilter
21 import android.net.ConnectivityManager
22 import android.net.Network
23 import android.net.NetworkCapabilities
24 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
25 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
26 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
27 import android.net.NetworkCapabilities.TRANSPORT_WIFI
28 import android.net.NetworkRequest
29 import android.net.wifi.WifiInfo
30 import android.net.wifi.WifiManager
31 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
32 import com.android.systemui.broadcast.BroadcastDispatcher
33 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
34 import com.android.systemui.dagger.SysUISingleton
35 import com.android.systemui.dagger.qualifiers.Application
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.dagger.qualifiers.Main
38 import com.android.systemui.log.table.TableLogBuffer
39 import com.android.systemui.log.table.logDiffsForTable
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.repository.ConnectivityRepository
43 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.getMainOrUnderlyingWifiInfo
44 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
45 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.CARRIER_MERGED_INVALID_SUB_ID_REASON
46 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT
47 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED
48 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger
49 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger
50 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
51 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
52 import java.util.concurrent.Executor
53 import javax.inject.Inject
54 import kotlinx.coroutines.CoroutineDispatcher
55 import kotlinx.coroutines.CoroutineScope
56 import kotlinx.coroutines.ExperimentalCoroutinesApi
57 import kotlinx.coroutines.channels.awaitClose
58 import kotlinx.coroutines.flow.Flow
59 import kotlinx.coroutines.flow.MutableSharedFlow
60 import kotlinx.coroutines.flow.MutableStateFlow
61 import kotlinx.coroutines.flow.SharingStarted
62 import kotlinx.coroutines.flow.StateFlow
63 import kotlinx.coroutines.flow.asStateFlow
64 import kotlinx.coroutines.flow.distinctUntilChanged
65 import kotlinx.coroutines.flow.map
66 import kotlinx.coroutines.flow.mapLatest
67 import kotlinx.coroutines.flow.merge
68 import kotlinx.coroutines.flow.onEach
69 import kotlinx.coroutines.flow.onStart
70 import kotlinx.coroutines.flow.stateIn
71 import kotlinx.coroutines.launch
72 import kotlinx.coroutines.withContext
73 
74 /** Real implementation of [WifiRepository]. */
75 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
76 @OptIn(ExperimentalCoroutinesApi::class)
77 @SysUISingleton
78 @SuppressLint("MissingPermission")
79 class WifiRepositoryImpl
80 @Inject
81 constructor(
82     broadcastDispatcher: BroadcastDispatcher,
83     connectivityManager: ConnectivityManager,
84     connectivityRepository: ConnectivityRepository,
85     logger: WifiInputLogger,
86     @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
87     @Main mainExecutor: Executor,
88     @Background private val bgDispatcher: CoroutineDispatcher,
89     @Application private val scope: CoroutineScope,
90     private val wifiManager: WifiManager,
91 ) : WifiRepositoryDagger {
92 
93     override fun start() {
94         // There are two possible [WifiRepository] implementations: This class (old) and
95         // [WifiRepositoryFromTrackerLib] (new). While we migrate to the new class, we want this old
96         // class to still be running in the background so that we can collect logs and compare
97         // discrepancies. This #start method collects on the flows to ensure that the logs are
98         // collected.
99         scope.launch { isWifiEnabled.collect {} }
100         scope.launch { isWifiDefault.collect {} }
101         scope.launch { wifiNetwork.collect {} }
102         scope.launch { wifiActivity.collect {} }
103     }
104 
105     private val wifiStateChangeEvents: Flow<Unit> =
106         broadcastDispatcher
107             .broadcastFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION))
108             .onEach { logger.logIntent("WIFI_STATE_CHANGED_ACTION") }
109 
110     private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> =
111         MutableSharedFlow(extraBufferCapacity = 1)
112 
113     // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it
114     // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may
115     // have changed.
116     override val isWifiEnabled: StateFlow<Boolean> =
117         merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
118             .onStart { emit(Unit) }
119             .mapLatest { isWifiEnabled() }
120             .distinctUntilChanged()
121             .logDiffsForTable(
122                 wifiTableLogBuffer,
123                 columnPrefix = "",
124                 columnName = COL_NAME_IS_ENABLED,
125                 initialValue = false,
126             )
127             .stateIn(
128                 scope = scope,
129                 started = SharingStarted.Eagerly,
130                 initialValue = false,
131             )
132 
133     // [WifiManager.isWifiEnabled] is a blocking IPC call, so fetch it in the background.
134     private suspend fun isWifiEnabled(): Boolean =
135         withContext(bgDispatcher) { wifiManager.isWifiEnabled }
136 
137     override val isWifiDefault: StateFlow<Boolean> =
138         connectivityRepository.defaultConnections
139             // TODO(b/274493701): Should wifi be considered default if it's carrier merged?
140             .map { it.wifi.isDefault || it.carrierMerged.isDefault }
141             .distinctUntilChanged()
142             .logDiffsForTable(
143                 wifiTableLogBuffer,
144                 columnPrefix = "",
145                 columnName = COL_NAME_IS_DEFAULT,
146                 initialValue = false,
147             )
148             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
149 
150     override val wifiNetwork: StateFlow<WifiNetworkModel> =
151         conflatedCallbackFlow {
152                 var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
153 
154                 val callback =
155                     object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
156                         override fun onCapabilitiesChanged(
157                             network: Network,
158                             networkCapabilities: NetworkCapabilities
159                         ) {
160                             logger.logOnCapabilitiesChanged(
161                                 network,
162                                 networkCapabilities,
163                                 isDefaultNetworkCallback = false,
164                             )
165 
166                             wifiNetworkChangeEvents.tryEmit(Unit)
167 
168                             val wifiInfo =
169                                 networkCapabilities.getMainOrUnderlyingWifiInfo(connectivityManager)
170                             if (wifiInfo?.isPrimary == true) {
171                                 val wifiNetworkModel =
172                                     createWifiNetworkModel(
173                                         wifiInfo,
174                                         network,
175                                         networkCapabilities,
176                                         wifiManager,
177                                     )
178                                 currentWifi = wifiNetworkModel
179                                 trySend(wifiNetworkModel)
180                             }
181                         }
182 
183                         override fun onLost(network: Network) {
184                             logger.logOnLost(network, isDefaultNetworkCallback = false)
185 
186                             wifiNetworkChangeEvents.tryEmit(Unit)
187 
188                             val wifi = currentWifi
189                             if (
190                                 (wifi is WifiNetworkModel.Active &&
191                                     wifi.networkId == network.getNetId()) ||
192                                     (wifi is WifiNetworkModel.CarrierMerged &&
193                                         wifi.networkId == network.getNetId())
194                             ) {
195                                 val newNetworkModel = WifiNetworkModel.Inactive
196                                 currentWifi = newNetworkModel
197                                 trySend(newNetworkModel)
198                             }
199                         }
200                     }
201 
202                 connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback)
203 
204                 awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
205             }
206             .distinctUntilChanged()
207             .logDiffsForTable(
208                 wifiTableLogBuffer,
209                 columnPrefix = "",
210                 initialValue = WIFI_NETWORK_DEFAULT,
211             )
212             // There will be multiple wifi icons in different places that will frequently
213             // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures
214             // that new subscribes will get the latest value immediately upon subscription.
215             // Otherwise, the views could show stale data. See b/244173280.
216             .stateIn(
217                 scope,
218                 started = SharingStarted.WhileSubscribed(),
219                 initialValue = WIFI_NETWORK_DEFAULT,
220             )
221 
222     // Secondary networks can only be supported by [WifiRepositoryViaTrackerLib].
223     override val secondaryNetworks: StateFlow<List<WifiNetworkModel>> =
224         MutableStateFlow(emptyList<WifiNetworkModel>()).asStateFlow()
225 
226     override val wifiActivity: StateFlow<DataActivityModel> =
227         WifiRepositoryHelper.createActivityFlow(
228             wifiManager,
229             mainExecutor,
230             scope,
231             wifiTableLogBuffer,
232             logger::logActivity,
233         )
234 
235     override val wifiScanResults: StateFlow<List<WifiScanEntry>> =
236         WifiRepositoryHelper.createNetworkScanFlow(
237             wifiManager,
238             scope,
239             bgDispatcher,
240             logger::logScanResults
241         )
242 
243     companion object {
244         // Start out with no known wifi network.
245         // Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an
246         // initial fetch to get a starting wifi network. But, it uses a deprecated API
247         // [WifiManager.getConnectionInfo()], and the deprecation doc indicates to just use
248         // [ConnectivityManager.NetworkCallback] results instead. So, for now we'll just rely on the
249         // NetworkCallback inside [wifiNetwork] for our wifi network information.
250         val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
251 
252         const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED
253 
254         private fun createWifiNetworkModel(
255             wifiInfo: WifiInfo,
256             network: Network,
257             networkCapabilities: NetworkCapabilities,
258             wifiManager: WifiManager,
259         ): WifiNetworkModel {
260             return if (wifiInfo.isCarrierMerged) {
261                 if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) {
262                     WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON)
263                 } else {
264                     WifiNetworkModel.CarrierMerged(
265                         networkId = network.getNetId(),
266                         subscriptionId = wifiInfo.subscriptionId,
267                         level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
268                         // The WiFi signal level returned by WifiManager#calculateSignalLevel start
269                         // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level
270                         // buckets count.
271                         numberOfLevels = wifiManager.maxSignalLevel + 1,
272                     )
273                 }
274             } else {
275                 WifiNetworkModel.Active(
276                     network.getNetId(),
277                     isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED),
278                     level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
279                     wifiInfo.ssid,
280                     // This repository doesn't support any hotspot information.
281                     WifiNetworkModel.HotspotDeviceType.NONE,
282                     wifiInfo.isPasspointAp,
283                     wifiInfo.isOsuAp,
284                     wifiInfo.passpointProviderFriendlyName
285                 )
286             }
287         }
288 
289         private val WIFI_NETWORK_CALLBACK_REQUEST: NetworkRequest =
290             NetworkRequest.Builder()
291                 .clearCapabilities()
292                 .addCapability(NET_CAPABILITY_NOT_VPN)
293                 .addTransportType(TRANSPORT_WIFI)
294                 .addTransportType(TRANSPORT_CELLULAR)
295                 .build()
296     }
297 
298     @SysUISingleton
299     class Factory
300     @Inject
301     constructor(
302         private val broadcastDispatcher: BroadcastDispatcher,
303         private val connectivityManager: ConnectivityManager,
304         private val connectivityRepository: ConnectivityRepository,
305         private val logger: WifiInputLogger,
306         @WifiTableLog private val wifiTableLogBuffer: TableLogBuffer,
307         @Main private val mainExecutor: Executor,
308         @Background private val bgDispatcher: CoroutineDispatcher,
309         @Application private val scope: CoroutineScope,
310     ) {
311         fun create(wifiManager: WifiManager): WifiRepositoryImpl {
312             return WifiRepositoryImpl(
313                 broadcastDispatcher,
314                 connectivityManager,
315                 connectivityRepository,
316                 logger,
317                 wifiTableLogBuffer,
318                 mainExecutor,
319                 bgDispatcher,
320                 scope,
321                 wifiManager,
322             )
323         }
324     }
325 }
326