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