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.mobile.data.repository.demo 18 19 import android.content.Context 20 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID 21 import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET 22 import android.util.Log 23 import com.android.app.tracing.coroutines.launchTraced as launch 24 import com.android.settingslib.SignalIcon 25 import com.android.settingslib.mobile.MobileMappings 26 import com.android.settingslib.mobile.TelephonyIcons 27 import com.android.systemui.dagger.qualifiers.Background 28 import com.android.systemui.log.table.TableLogBufferFactory 29 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType 30 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType 31 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel 32 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository 33 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 34 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel 35 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile 36 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled 37 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE 38 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource 39 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel 40 import javax.inject.Inject 41 import kotlinx.coroutines.CoroutineScope 42 import kotlinx.coroutines.Job 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.MutableSharedFlow 45 import kotlinx.coroutines.flow.MutableStateFlow 46 import kotlinx.coroutines.flow.SharingStarted 47 import kotlinx.coroutines.flow.StateFlow 48 import kotlinx.coroutines.flow.filterNotNull 49 import kotlinx.coroutines.flow.flowOf 50 import kotlinx.coroutines.flow.map 51 import kotlinx.coroutines.flow.mapLatest 52 import kotlinx.coroutines.flow.onEach 53 import kotlinx.coroutines.flow.stateIn 54 55 /** This repository vends out data based on demo mode commands */ 56 class DemoMobileConnectionsRepository 57 @Inject 58 constructor( 59 private val mobileDataSource: DemoModeMobileConnectionDataSource, 60 private val wifiDataSource: DemoModeWifiDataSource, 61 @Background private val scope: CoroutineScope, 62 context: Context, 63 private val logFactory: TableLogBufferFactory, 64 ) : MobileConnectionsRepository { 65 66 private var mobileDemoCommandJob: Job? = null 67 private var wifiDemoCommandJob: Job? = null 68 69 private var carrierMergedSubId: Int? = null 70 71 private var connectionRepoCache = mutableMapOf<Int, CacheContainer>() 72 private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>() 73 val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) 74 75 private val _subscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) 76 override val subscriptions = 77 _subscriptions 78 .onEach { infos -> dropUnusedReposFromCache(infos) } 79 .stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value) 80 81 private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) { 82 // Remove any connection repository from the cache that isn't in the new set of IDs. They 83 // will get garbage collected once their subscribers go away 84 val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } 85 86 connectionRepoCache = 87 connectionRepoCache 88 .filter { currentValidSubscriptionIds.contains(it.key) } 89 .toMutableMap() 90 } 91 92 private fun maybeCreateSubscription(subId: Int) { 93 if (!subscriptionInfoCache.containsKey(subId)) { 94 SubscriptionModel( 95 subscriptionId = subId, 96 isOpportunistic = false, 97 carrierName = DEFAULT_CARRIER_NAME, 98 profileClass = PROFILE_CLASS_UNSET, 99 ) 100 .also { subscriptionInfoCache[subId] = it } 101 102 _subscriptions.value = subscriptionInfoCache.values.toList() 103 } 104 } 105 106 // TODO(b/261029387): add a command for this value 107 override val activeMobileDataSubscriptionId = 108 subscriptions 109 .mapLatest { infos -> 110 // For now, active is just the first in the list 111 infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID 112 } 113 .stateIn( 114 scope, 115 SharingStarted.WhileSubscribed(), 116 subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID, 117 ) 118 119 override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> = 120 activeMobileDataSubscriptionId 121 .map { getRepoForSubId(it) } 122 .stateIn( 123 scope, 124 SharingStarted.WhileSubscribed(), 125 getRepoForSubId(activeMobileDataSubscriptionId.value), 126 ) 127 128 // TODO(b/261029387): consider adding a demo command for this 129 override val activeSubChangedInGroupEvent: Flow<Unit> = flowOf() 130 131 /** Demo mode doesn't currently support modifications to the mobile mappings */ 132 override val defaultDataSubRatConfig = 133 MutableStateFlow(MobileMappings.Config.readConfig(context)) 134 135 override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G) 136 137 // TODO(b/339023069): demo command for device-based emergency calls state 138 override val isDeviceEmergencyCallCapable: StateFlow<Boolean> = MutableStateFlow(false) 139 140 override val isAnySimSecure: Flow<Boolean> = flowOf(getIsAnySimSecure()) 141 142 override fun getIsAnySimSecure(): Boolean = false 143 144 override val defaultMobileIconMapping = MutableStateFlow(TelephonyIcons.ICON_NAME_TO_ICON) 145 146 /** 147 * In order to maintain compatibility with the old demo mode shell command API, reverse the 148 * [MobileMappings] lookup from (NetworkType: String -> Icon: MobileIconGroup), so that we can 149 * parse the string from the command line into a preferred icon group, and send _a_ valid 150 * network type for that icon through the pipeline. 151 * 152 * Note: collisions don't matter here, because the data source (the command line) only cares 153 * about the resulting icon, not the underlying network type. 154 */ 155 private val mobileMappingsReverseLookup: StateFlow<Map<SignalIcon.MobileIconGroup, String>> = 156 defaultMobileIconMapping 157 .mapLatest { networkToIconMap -> networkToIconMap.reverse() } 158 .stateIn( 159 scope, 160 SharingStarted.WhileSubscribed(), 161 defaultMobileIconMapping.value.reverse(), 162 ) 163 164 private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key } 165 166 // TODO(b/261029387): add a command for this value 167 override val defaultDataSubId: MutableStateFlow<Int?> = MutableStateFlow(null) 168 169 // TODO(b/261029387): not yet supported 170 override val mobileIsDefault: StateFlow<Boolean> = MutableStateFlow(true) 171 172 // TODO(b/261029387): not yet supported 173 override val hasCarrierMergedConnection = MutableStateFlow(false) 174 175 // TODO(b/261029387): not yet supported 176 override val defaultConnectionIsValidated: StateFlow<Boolean> = MutableStateFlow(true) 177 178 override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { 179 val current = connectionRepoCache[subId]?.repo 180 if (current != null) { 181 return current 182 } 183 184 val new = createDemoMobileConnectionRepo(subId) 185 connectionRepoCache[subId] = new 186 return new.repo 187 } 188 189 private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer { 190 val tableLogBuffer = 191 logFactory.getOrCreate("DemoMobileConnectionLog[$subId]", MOBILE_CONNECTION_BUFFER_SIZE) 192 193 val repo = DemoMobileConnectionRepository(subId, tableLogBuffer, scope) 194 return CacheContainer(repo, lastMobileState = null) 195 } 196 197 fun startProcessingCommands() { 198 mobileDemoCommandJob = 199 scope.launch { 200 mobileDataSource.mobileEvents.filterNotNull().collect { event -> 201 processMobileEvent(event) 202 } 203 } 204 wifiDemoCommandJob = 205 scope.launch { 206 wifiDataSource.wifiEvents.filterNotNull().collect { event -> 207 processWifiEvent(event) 208 } 209 } 210 } 211 212 fun stopProcessingCommands() { 213 mobileDemoCommandJob?.cancel() 214 wifiDemoCommandJob?.cancel() 215 _subscriptions.value = listOf() 216 connectionRepoCache.clear() 217 subscriptionInfoCache.clear() 218 } 219 220 override suspend fun isInEcmMode(): Boolean = false 221 222 private fun processMobileEvent(event: FakeNetworkEventModel) { 223 when (event) { 224 is Mobile -> { 225 processEnabledMobileState(event) 226 } 227 is MobileDisabled -> { 228 maybeRemoveSubscription(event.subId) 229 } 230 } 231 } 232 233 private fun processWifiEvent(event: FakeWifiEventModel) { 234 when (event) { 235 is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged() 236 is FakeWifiEventModel.Wifi -> disableCarrierMerged() 237 is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) 238 } 239 } 240 241 private fun processEnabledMobileState(event: Mobile) { 242 // get or create the connection repo, and set its values 243 val subId = event.subId ?: DEFAULT_SUB_ID 244 maybeCreateSubscription(subId) 245 246 val connection = getRepoForSubId(subId) 247 connectionRepoCache[subId]?.lastMobileState = event 248 249 // TODO(b/261029387): until we have a command, use the most recent subId 250 defaultDataSubId.value = subId 251 252 connection.processDemoMobileEvent(event, event.dataType.toResolvedNetworkType()) 253 } 254 255 private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { 256 // The new carrier merged connection is for a different sub ID, so disable carrier merged 257 // for the current (now old) sub 258 if (carrierMergedSubId != event.subscriptionId) { 259 disableCarrierMerged() 260 } 261 262 // get or create the connection repo, and set its values 263 val subId = event.subscriptionId 264 maybeCreateSubscription(subId) 265 carrierMergedSubId = subId 266 267 // TODO(b/261029387): until we have a command, use the most recent subId 268 defaultDataSubId.value = subId 269 270 val connection = getRepoForSubId(subId) 271 connection.processCarrierMergedEvent(event) 272 } 273 274 private fun maybeRemoveSubscription(subId: Int?) { 275 if (_subscriptions.value.isEmpty()) { 276 // Nothing to do here 277 return 278 } 279 280 val finalSubId = 281 subId 282 ?: run { 283 // For sake of usability, we can allow for no subId arg if there is only one 284 // subscription 285 if (_subscriptions.value.size > 1) { 286 Log.d( 287 TAG, 288 "processDisabledMobileState: Unable to infer subscription to " + 289 "disable. Specify subId using '-e slot <subId>'" + 290 "Known subIds: [${subIdsString()}]", 291 ) 292 return 293 } 294 295 // Use the only existing subscription as our arg, since there is only one 296 _subscriptions.value[0].subscriptionId 297 } 298 299 removeSubscription(finalSubId) 300 } 301 302 private fun disableCarrierMerged() { 303 val currentCarrierMergedSubId = carrierMergedSubId ?: return 304 305 // If this sub ID was previously not carrier merged, we should reset it to its previous 306 // connection. 307 val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState 308 if (lastMobileState != null) { 309 processEnabledMobileState(lastMobileState) 310 } else { 311 // Otherwise, just remove the subscription entirely 312 removeSubscription(currentCarrierMergedSubId) 313 } 314 } 315 316 private fun removeSubscription(subId: Int) { 317 val currentSubscriptions = _subscriptions.value 318 subscriptionInfoCache.remove(subId) 319 _subscriptions.value = currentSubscriptions.filter { it.subscriptionId != subId } 320 } 321 322 private fun subIdsString(): String = 323 _subscriptions.value.joinToString(",") { it.subscriptionId.toString() } 324 325 private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { 326 val key = mobileMappingsReverseLookup.value[this] ?: "dis" 327 return DefaultNetworkType(key) 328 } 329 330 companion object { 331 private const val TAG = "DemoMobileConnectionsRepo" 332 333 private const val DEFAULT_SUB_ID = 1 334 private const val DEFAULT_CARRIER_NAME = "demo carrier" 335 } 336 } 337 338 class CacheContainer( 339 var repo: DemoMobileConnectionRepository, 340 /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */ 341 var lastMobileState: Mobile?, 342 ) 343