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