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