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