• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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