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