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