1 /*
<lambda>null2  * 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.shared.data.repository
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import android.net.ConnectivityManager
22 import android.net.Network
23 import android.net.NetworkCapabilities
24 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
25 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
26 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
27 import android.net.NetworkCapabilities.TRANSPORT_WIFI
28 import android.net.vcn.VcnTransportInfo
29 import android.net.wifi.WifiInfo
30 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
31 import androidx.annotation.ArrayRes
32 import androidx.annotation.VisibleForTesting
33 import com.android.systemui.Dumpable
34 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
35 import com.android.systemui.dagger.SysUISingleton
36 import com.android.systemui.dagger.qualifiers.Application
37 import com.android.systemui.dump.DumpManager
38 import com.android.systemui.res.R
39 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
40 import com.android.systemui.statusbar.pipeline.shared.ConnectivityInputLogger
41 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
42 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
43 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
44 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.CarrierMerged
45 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.Ethernet
46 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.Mobile
47 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel.Wifi
48 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.getMainOrUnderlyingWifiInfo
49 import com.android.systemui.tuner.TunerService
50 import java.io.PrintWriter
51 import javax.inject.Inject
52 import kotlinx.coroutines.CoroutineScope
53 import kotlinx.coroutines.channels.awaitClose
54 import kotlinx.coroutines.flow.SharedFlow
55 import kotlinx.coroutines.flow.SharingStarted
56 import kotlinx.coroutines.flow.StateFlow
57 import kotlinx.coroutines.flow.distinctUntilChanged
58 import kotlinx.coroutines.flow.map
59 import kotlinx.coroutines.flow.onEach
60 import kotlinx.coroutines.flow.shareIn
61 import kotlinx.coroutines.flow.stateIn
62 
63 /**
64  * Provides data related to the connectivity state that needs to be shared across multiple different
65  * types of connectivity (wifi, mobile, ethernet, etc.)
66  */
67 interface ConnectivityRepository {
68     /** Observable for the current set of connectivity icons that should be force-hidden. */
69     val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>>
70 
71     /** Observable for which connection(s) are currently default. */
72     val defaultConnections: StateFlow<DefaultConnectionModel>
73 
74     /**
75      * Subscription ID of the [VcnTransportInfo] for the default connection.
76      *
77      * If the default network has a [VcnTransportInfo], then that transport info contains a subId of
78      * the VCN. When VCN is connected and default, this subId is what SystemUI will care about. In
79      * cases where telephony's activeDataSubscriptionId differs from this value, it is expected to
80      * eventually catch up and reflect what is represented here in the VcnTransportInfo.
81      */
82     val vcnSubId: StateFlow<Int?>
83 }
84 
85 @SuppressLint("MissingPermission")
86 @SysUISingleton
87 class ConnectivityRepositoryImpl
88 @Inject
89 constructor(
90     private val connectivityManager: ConnectivityManager,
91     private val connectivitySlots: ConnectivitySlots,
92     context: Context,
93     dumpManager: DumpManager,
94     logger: ConnectivityInputLogger,
95     @Application scope: CoroutineScope,
96     tunerService: TunerService,
97 ) : ConnectivityRepository, Dumpable {
98     init {
99         dumpManager.registerNormalDumpable("ConnectivityRepository", this)
100     }
101 
102     // The default set of hidden icons to use if we don't get any from [TunerService].
103     private val defaultHiddenIcons: Set<ConnectivitySlot> =
104         context.resources
105             .getStringArray(DEFAULT_HIDDEN_ICONS_RESOURCE)
106             .asList()
107             .toSlotSet(connectivitySlots)
108 
109     override val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>> =
<lambda>null110         conflatedCallbackFlow {
111                 val callback =
112                     object : TunerService.Tunable {
113                         override fun onTuningChanged(key: String, newHideList: String?) {
114                             if (key != HIDDEN_ICONS_TUNABLE_KEY) {
115                                 return
116                             }
117                             logger.logTuningChanged(newHideList)
118 
119                             val outputList =
120                                 newHideList?.split(",")?.toSlotSet(connectivitySlots)
121                                     ?: defaultHiddenIcons
122                             trySend(outputList)
123                         }
124                     }
125                 tunerService.addTunable(callback, HIDDEN_ICONS_TUNABLE_KEY)
126 
127                 awaitClose { tunerService.removeTunable(callback) }
128             }
129             .stateIn(
130                 scope,
131                 started = SharingStarted.WhileSubscribed(),
132                 initialValue = defaultHiddenIcons
133             )
134 
135     private val defaultNetworkCapabilities: SharedFlow<NetworkCapabilities?> =
<lambda>null136         conflatedCallbackFlow {
137                 val callback =
138                     object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
139                         override fun onLost(network: Network) {
140                             logger.logOnDefaultLost(network)
141                             trySend(null)
142                         }
143 
144                         override fun onCapabilitiesChanged(
145                             network: Network,
146                             networkCapabilities: NetworkCapabilities,
147                         ) {
148                             logger.logOnDefaultCapabilitiesChanged(network, networkCapabilities)
149                             trySend(networkCapabilities)
150                         }
151                     }
152 
153                 connectivityManager.registerDefaultNetworkCallback(callback)
154 
155                 awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
156             }
157             .shareIn(scope, SharingStarted.WhileSubscribed())
158 
159     override val vcnSubId: StateFlow<Int?> =
160         defaultNetworkCapabilities
networkCapabilitiesnull161             .map { networkCapabilities ->
162                 networkCapabilities?.run {
163                     val subId = (transportInfo as? VcnTransportInfo)?.subId
164                     // Never return an INVALID_SUBSCRIPTION_ID (-1)
165                     if (subId != INVALID_SUBSCRIPTION_ID) {
166                         subId
167                     } else {
168                         null
169                     }
170                 }
171             }
172             .distinctUntilChanged()
173             /* A note for logging: we use -2 here since -1 == INVALID_SUBSCRIPTION_ID */
<lambda>null174             .onEach { logger.logVcnSubscriptionId(it ?: -2) }
175             .stateIn(scope, SharingStarted.Eagerly, null)
176 
177     @SuppressLint("MissingPermission")
178     override val defaultConnections: StateFlow<DefaultConnectionModel> =
179         defaultNetworkCapabilities
networkCapabilitiesnull180             .map { networkCapabilities ->
181                 if (networkCapabilities == null) {
182                     // The system no longer has a default network, so everything is
183                     // non-default.
184                     DefaultConnectionModel(
185                         Wifi(isDefault = false),
186                         Mobile(isDefault = false),
187                         CarrierMerged(isDefault = false),
188                         Ethernet(isDefault = false),
189                         isValidated = false,
190                     )
191                 } else {
192                     val wifiInfo =
193                         networkCapabilities.getMainOrUnderlyingWifiInfo(connectivityManager)
194 
195                     val isWifiDefault =
196                         networkCapabilities.hasTransport(TRANSPORT_WIFI) || wifiInfo != null
197                     val isMobileDefault = networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
198                     val isCarrierMergedDefault = wifiInfo?.isCarrierMerged == true
199                     val isEthernetDefault = networkCapabilities.hasTransport(TRANSPORT_ETHERNET)
200 
201                     val isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)
202 
203                     DefaultConnectionModel(
204                         Wifi(isWifiDefault),
205                         Mobile(isMobileDefault),
206                         CarrierMerged(isCarrierMergedDefault),
207                         Ethernet(isEthernetDefault),
208                         isValidated,
209                     )
210                 }
211             }
212             .distinctUntilChanged()
<lambda>null213             .onEach { logger.logDefaultConnectionsChanged(it) }
214             .stateIn(scope, SharingStarted.Eagerly, DefaultConnectionModel())
215 
dumpnull216     override fun dump(pw: PrintWriter, args: Array<out String>) {
217         pw.apply { println("defaultHiddenIcons=$defaultHiddenIcons") }
218     }
219 
220     companion object {
221         @VisibleForTesting
222         internal const val HIDDEN_ICONS_TUNABLE_KEY = StatusBarIconController.ICON_HIDE_LIST
223         @VisibleForTesting
224         @ArrayRes
225         internal val DEFAULT_HIDDEN_ICONS_RESOURCE = R.array.config_statusBarIconsToExclude
226 
227         /** Converts a list of string slot names to a set of [ConnectivitySlot] instances. */
Listnull228         private fun List<String>.toSlotSet(
229             connectivitySlots: ConnectivitySlots
230         ): Set<ConnectivitySlot> {
231             return this.filter { it.isNotBlank() }
232                 .mapNotNull { connectivitySlots.getSlotFromName(it) }
233                 .toSet()
234         }
235 
236         /**
237          * Returns a [WifiInfo] object from the capabilities if it has one, or null if there is no
238          * underlying wifi network.
239          *
240          * This will return a valid [WifiInfo] object if wifi is the main transport **or** wifi is
241          * an underlying transport. This is important for carrier merged networks, where the main
242          * transport info is *not* wifi, but the underlying transport info *is* wifi. We want to
243          * always use [WifiInfo] if it's available, so we need to check the underlying transport
244          * info.
245          */
getMainOrUnderlyingWifiInfonull246         fun NetworkCapabilities.getMainOrUnderlyingWifiInfo(
247             connectivityManager: ConnectivityManager,
248         ): WifiInfo? {
249             val mainWifiInfo = this.getMainWifiInfo()
250             if (mainWifiInfo != null) {
251                 return mainWifiInfo
252             }
253             // Only CELLULAR networks may have underlying wifi information that's relevant to SysUI,
254             // so skip the underlying network check if it's not CELLULAR.
255             if (!this.hasTransport(TRANSPORT_CELLULAR)) {
256                 return mainWifiInfo
257             }
258 
259             // Some connections, like VPN connections, may have underlying networks that are
260             // eventually traced to a wifi or carrier merged connection. So, check those underlying
261             // networks for possible wifi information as well. See b/225902574.
262             return this.underlyingNetworks?.firstNotNullOfOrNull { underlyingNetwork ->
263                 connectivityManager.getNetworkCapabilities(underlyingNetwork)?.getMainWifiInfo()
264             }
265         }
266 
267         /**
268          * Checks the network capabilities for wifi info, but does *not* check the underlying
269          * networks. See [getMainOrUnderlyingWifiInfo].
270          */
getMainWifiInfonull271         private fun NetworkCapabilities.getMainWifiInfo(): WifiInfo? {
272             // Wifi info can either come from a WIFI Transport, or from a CELLULAR transport for
273             // virtual networks like VCN.
274             val canHaveWifiInfo =
275                 this.hasTransport(TRANSPORT_CELLULAR) || this.hasTransport(TRANSPORT_WIFI)
276             if (!canHaveWifiInfo) {
277                 return null
278             }
279 
280             return when (val currentTransportInfo = transportInfo) {
281                 // This VcnTransportInfo logic is copied from
282                 // [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of
283                 // re-used because it makes the logic here clearer, and because the method will be
284                 // removed once this pipeline is fully launched.
285                 is VcnTransportInfo -> currentTransportInfo.wifiInfo
286                 is WifiInfo -> currentTransportInfo
287                 else -> null
288             }
289         }
290     }
291 }
292