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