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