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
18 
19 import android.os.Bundle
20 import androidx.annotation.VisibleForTesting
21 import com.android.settingslib.SignalIcon
22 import com.android.settingslib.mobile.MobileMappings
23 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.dagger.qualifiers.Application
26 import com.android.systemui.demomode.DemoMode
27 import com.android.systemui.demomode.DemoModeController
28 import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel
29 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
30 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
31 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
32 import javax.inject.Inject
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.ExperimentalCoroutinesApi
35 import kotlinx.coroutines.channels.awaitClose
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.SharingStarted
38 import kotlinx.coroutines.flow.StateFlow
39 import kotlinx.coroutines.flow.flatMapLatest
40 import kotlinx.coroutines.flow.mapLatest
41 import kotlinx.coroutines.flow.stateIn
42 
43 /**
44  * A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and
45  * Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], which
46  * switches based on the latest information from [DemoModeController], and switches every flow in
47  * the interface to point to the currently-active provider. This allows us to put the demo mode
48  * interface in its own repository, completely separate from the real version, while still using all
49  * of the prod implementations for the rest of the pipeline (interactors and onward). Looks
50  * something like this:
51  * ```
52  * RealRepository
53  *                 │
54  *                 ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
55  *                 │
56  * DemoRepository
57  * ```
58  *
59  * NOTE: because the UI layer for mobile icons relies on a nested-repository structure, it is likely
60  * that we will have to drain the subscription list whenever demo mode changes. Otherwise if a real
61  * subscription list [1] is replaced with a demo subscription list [1], the view models will not see
62  * a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo
63  * implementation.
64  */
65 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
66 @OptIn(ExperimentalCoroutinesApi::class)
67 @SysUISingleton
68 class MobileRepositorySwitcher
69 @Inject
70 constructor(
71     @Application scope: CoroutineScope,
72     val realRepository: MobileConnectionsRepositoryImpl,
73     val demoMobileConnectionsRepository: DemoMobileConnectionsRepository,
74     demoModeController: DemoModeController,
75 ) : MobileConnectionsRepository {
76 
77     val isDemoMode: StateFlow<Boolean> =
78         conflatedCallbackFlow {
79                 val callback =
80                     object : DemoMode {
81                         override fun dispatchDemoCommand(command: String?, args: Bundle?) {
82                             // Nothing, we just care about on/off
83                         }
84 
85                         override fun onDemoModeStarted() {
86                             demoMobileConnectionsRepository.startProcessingCommands()
87                             trySend(true)
88                         }
89 
90                         override fun onDemoModeFinished() {
91                             demoMobileConnectionsRepository.stopProcessingCommands()
92                             trySend(false)
93                         }
94                     }
95 
96                 demoModeController.addCallback(callback)
97                 awaitClose { demoModeController.removeCallback(callback) }
98             }
99             .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
100 
101     // Convenient definition flow for the currently active repo (based on demo mode or not)
102     @VisibleForTesting
103     internal val activeRepo: StateFlow<MobileConnectionsRepository> =
104         isDemoMode
105             .mapLatest { demoMode ->
106                 if (demoMode) {
107                     demoMobileConnectionsRepository
108                 } else {
109                     realRepository
110                 }
111             }
112             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository)
113 
114     override val subscriptions: StateFlow<List<SubscriptionModel>> =
115         activeRepo
116             .flatMapLatest { it.subscriptions }
117             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.subscriptions.value)
118 
119     override val activeMobileDataSubscriptionId: StateFlow<Int?> =
120         activeRepo
121             .flatMapLatest { it.activeMobileDataSubscriptionId }
122             .stateIn(
123                 scope,
124                 SharingStarted.WhileSubscribed(),
125                 realRepository.activeMobileDataSubscriptionId.value
126             )
127 
128     override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
129         activeRepo
130             .flatMapLatest { it.activeMobileDataRepository }
131             .stateIn(
132                 scope,
133                 SharingStarted.WhileSubscribed(),
134                 realRepository.activeMobileDataRepository.value
135             )
136 
137     override val activeSubChangedInGroupEvent: Flow<Unit> =
138         activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent }
139 
140     override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
141         activeRepo
142             .flatMapLatest { it.defaultDataSubRatConfig }
143             .stateIn(
144                 scope,
145                 SharingStarted.WhileSubscribed(),
146                 realRepository.defaultDataSubRatConfig.value
147             )
148 
149     override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> =
150         activeRepo.flatMapLatest { it.defaultMobileIconMapping }
151 
152     override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
153         activeRepo.flatMapLatest { it.defaultMobileIconGroup }
154 
155     override val deviceServiceState: StateFlow<ServiceStateModel?> =
156         activeRepo
157             .flatMapLatest { it.deviceServiceState }
158             .stateIn(
159                 scope,
160                 SharingStarted.WhileSubscribed(),
161                 realRepository.deviceServiceState.value
162             )
163 
164     override val isAnySimSecure: Flow<Boolean> = activeRepo.flatMapLatest { it.isAnySimSecure }
165     override fun getIsAnySimSecure(): Boolean = activeRepo.value.getIsAnySimSecure()
166 
167     override val defaultDataSubId: StateFlow<Int> =
168         activeRepo
169             .flatMapLatest { it.defaultDataSubId }
170             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
171 
172     override val mobileIsDefault: StateFlow<Boolean> =
173         activeRepo
174             .flatMapLatest { it.mobileIsDefault }
175             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.mobileIsDefault.value)
176 
177     override val hasCarrierMergedConnection: StateFlow<Boolean> =
178         activeRepo
179             .flatMapLatest { it.hasCarrierMergedConnection }
180             .stateIn(
181                 scope,
182                 SharingStarted.WhileSubscribed(),
183                 realRepository.hasCarrierMergedConnection.value,
184             )
185 
186     override val defaultConnectionIsValidated: StateFlow<Boolean> =
187         activeRepo
188             .flatMapLatest { it.defaultConnectionIsValidated }
189             .stateIn(
190                 scope,
191                 SharingStarted.WhileSubscribed(),
192                 realRepository.defaultConnectionIsValidated.value
193             )
194 
195     override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
196         if (isDemoMode.value) {
197             return demoMobileConnectionsRepository.getRepoForSubId(subId)
198         }
199         return realRepository.getRepoForSubId(subId)
200     }
201 
202     override suspend fun isInEcmMode(): Boolean =
203         if (isDemoMode.value) {
204             demoMobileConnectionsRepository.isInEcmMode()
205         } else {
206             realRepository.isInEcmMode()
207         }
208 }
209