• 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
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.qualifiers.Application
25 import com.android.systemui.demomode.DemoMode
26 import com.android.systemui.demomode.DemoModeController
27 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
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 class MobileRepositorySwitcher
67 @Inject
68 constructor(
69     @Application scope: CoroutineScope,
70     val realRepository: MobileConnectionsRepositoryImpl,
71     val demoMobileConnectionsRepository: DemoMobileConnectionsRepository,
72     demoModeController: DemoModeController,
73 ) : MobileConnectionsRepository {
74 
75     val isDemoMode: StateFlow<Boolean> =
76         conflatedCallbackFlow {
77                 val callback =
78                     object : DemoMode {
79                         override fun dispatchDemoCommand(command: String?, args: Bundle?) {
80                             // Nothing, we just care about on/off
81                         }
82 
83                         override fun onDemoModeStarted() {
84                             demoMobileConnectionsRepository.startProcessingCommands()
85                             trySend(true)
86                         }
87 
88                         override fun onDemoModeFinished() {
89                             demoMobileConnectionsRepository.stopProcessingCommands()
90                             trySend(false)
91                         }
92                     }
93 
94                 demoModeController.addCallback(callback)
95                 awaitClose { demoModeController.removeCallback(callback) }
96             }
97             .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
98 
99     // Convenient definition flow for the currently active repo (based on demo mode or not)
100     @VisibleForTesting
101     internal val activeRepo: StateFlow<MobileConnectionsRepository> =
102         isDemoMode
103             .mapLatest { demoMode ->
104                 if (demoMode) {
105                     demoMobileConnectionsRepository
106                 } else {
107                     realRepository
108                 }
109             }
110             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository)
111 
112     override val subscriptions: StateFlow<List<SubscriptionModel>> =
113         activeRepo
114             .flatMapLatest { it.subscriptions }
115             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.subscriptions.value)
116 
117     override val activeMobileDataSubscriptionId: StateFlow<Int?> =
118         activeRepo
119             .flatMapLatest { it.activeMobileDataSubscriptionId }
120             .stateIn(
121                 scope,
122                 SharingStarted.WhileSubscribed(),
123                 realRepository.activeMobileDataSubscriptionId.value
124             )
125 
126     override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
127         activeRepo
128             .flatMapLatest { it.activeMobileDataRepository }
129             .stateIn(
130                 scope,
131                 SharingStarted.WhileSubscribed(),
132                 realRepository.activeMobileDataRepository.value
133             )
134 
135     override val activeSubChangedInGroupEvent: Flow<Unit> =
136         activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent }
137 
138     override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
139         activeRepo
140             .flatMapLatest { it.defaultDataSubRatConfig }
141             .stateIn(
142                 scope,
143                 SharingStarted.WhileSubscribed(),
144                 realRepository.defaultDataSubRatConfig.value
145             )
146 
147     override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> =
148         activeRepo.flatMapLatest { it.defaultMobileIconMapping }
149 
150     override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
151         activeRepo.flatMapLatest { it.defaultMobileIconGroup }
152 
153     override val defaultDataSubId: StateFlow<Int> =
154         activeRepo
155             .flatMapLatest { it.defaultDataSubId }
156             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
157 
158     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
159         activeRepo
160             .flatMapLatest { it.defaultMobileNetworkConnectivity }
161             .stateIn(
162                 scope,
163                 SharingStarted.WhileSubscribed(),
164                 realRepository.defaultMobileNetworkConnectivity.value
165             )
166 
167     override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
168         if (isDemoMode.value) {
169             return demoMobileConnectionsRepository.getRepoForSubId(subId)
170         }
171         return realRepository.getRepoForSubId(subId)
172     }
173 }
174