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