• 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.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