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