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