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