| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.statusbar.pipeline.mobile.data.repository |
| |
| import android.os.Bundle |
| import androidx.annotation.VisibleForTesting |
| import com.android.settingslib.SignalIcon |
| import com.android.settingslib.mobile.MobileMappings |
| import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow |
| import com.android.systemui.dagger.qualifiers.Application |
| import com.android.systemui.demomode.DemoMode |
| import com.android.systemui.demomode.DemoModeController |
| import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel |
| import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository |
| import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl |
| import javax.inject.Inject |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.ExperimentalCoroutinesApi |
| import kotlinx.coroutines.channels.awaitClose |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.SharingStarted |
| import kotlinx.coroutines.flow.StateFlow |
| import kotlinx.coroutines.flow.flatMapLatest |
| import kotlinx.coroutines.flow.mapLatest |
| import kotlinx.coroutines.flow.stateIn |
| |
| /** |
| * A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and |
| * Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], which |
| * switches based on the latest information from [DemoModeController], and switches every flow in |
| * the interface to point to the currently-active provider. This allows us to put the demo mode |
| * interface in its own repository, completely separate from the real version, while still using all |
| * of the prod implementations for the rest of the pipeline (interactors and onward). Looks |
| * something like this: |
| * ``` |
| * RealRepository |
| * │ |
| * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel |
| * │ |
| * DemoRepository |
| * ``` |
| * |
| * NOTE: because the UI layer for mobile icons relies on a nested-repository structure, it is likely |
| * that we will have to drain the subscription list whenever demo mode changes. Otherwise if a real |
| * subscription list [1] is replaced with a demo subscription list [1], the view models will not see |
| * a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo |
| * implementation. |
| */ |
| @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") |
| @OptIn(ExperimentalCoroutinesApi::class) |
| class MobileRepositorySwitcher |
| @Inject |
| constructor( |
| @Application scope: CoroutineScope, |
| val realRepository: MobileConnectionsRepositoryImpl, |
| val demoMobileConnectionsRepository: DemoMobileConnectionsRepository, |
| demoModeController: DemoModeController, |
| ) : MobileConnectionsRepository { |
| |
| val isDemoMode: StateFlow<Boolean> = |
| conflatedCallbackFlow { |
| val callback = |
| object : DemoMode { |
| override fun dispatchDemoCommand(command: String?, args: Bundle?) { |
| // Nothing, we just care about on/off |
| } |
| |
| override fun onDemoModeStarted() { |
| demoMobileConnectionsRepository.startProcessingCommands() |
| trySend(true) |
| } |
| |
| override fun onDemoModeFinished() { |
| demoMobileConnectionsRepository.stopProcessingCommands() |
| trySend(false) |
| } |
| } |
| |
| demoModeController.addCallback(callback) |
| awaitClose { demoModeController.removeCallback(callback) } |
| } |
| .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode) |
| |
| // Convenient definition flow for the currently active repo (based on demo mode or not) |
| @VisibleForTesting |
| internal val activeRepo: StateFlow<MobileConnectionsRepository> = |
| isDemoMode |
| .mapLatest { demoMode -> |
| if (demoMode) { |
| demoMobileConnectionsRepository |
| } else { |
| realRepository |
| } |
| } |
| .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository) |
| |
| override val subscriptions: StateFlow<List<SubscriptionModel>> = |
| activeRepo |
| .flatMapLatest { it.subscriptions } |
| .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.subscriptions.value) |
| |
| override val activeMobileDataSubscriptionId: StateFlow<Int?> = |
| activeRepo |
| .flatMapLatest { it.activeMobileDataSubscriptionId } |
| .stateIn( |
| scope, |
| SharingStarted.WhileSubscribed(), |
| realRepository.activeMobileDataSubscriptionId.value |
| ) |
| |
| override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> = |
| activeRepo |
| .flatMapLatest { it.activeMobileDataRepository } |
| .stateIn( |
| scope, |
| SharingStarted.WhileSubscribed(), |
| realRepository.activeMobileDataRepository.value |
| ) |
| |
| override val activeSubChangedInGroupEvent: Flow<Unit> = |
| activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent } |
| |
| override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> = |
| activeRepo |
| .flatMapLatest { it.defaultDataSubRatConfig } |
| .stateIn( |
| scope, |
| SharingStarted.WhileSubscribed(), |
| realRepository.defaultDataSubRatConfig.value |
| ) |
| |
| override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> = |
| activeRepo.flatMapLatest { it.defaultMobileIconMapping } |
| |
| override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> = |
| activeRepo.flatMapLatest { it.defaultMobileIconGroup } |
| |
| override val defaultDataSubId: StateFlow<Int> = |
| activeRepo |
| .flatMapLatest { it.defaultDataSubId } |
| .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value) |
| |
| override val mobileIsDefault: StateFlow<Boolean> = |
| activeRepo |
| .flatMapLatest { it.mobileIsDefault } |
| .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.mobileIsDefault.value) |
| |
| override val hasCarrierMergedConnection: StateFlow<Boolean> = |
| activeRepo |
| .flatMapLatest { it.hasCarrierMergedConnection } |
| .stateIn( |
| scope, |
| SharingStarted.WhileSubscribed(), |
| realRepository.hasCarrierMergedConnection.value, |
| ) |
| |
| override val defaultConnectionIsValidated: StateFlow<Boolean> = |
| activeRepo |
| .flatMapLatest { it.defaultConnectionIsValidated } |
| .stateIn( |
| scope, |
| SharingStarted.WhileSubscribed(), |
| realRepository.defaultConnectionIsValidated.value |
| ) |
| |
| override fun getRepoForSubId(subId: Int): MobileConnectionRepository { |
| if (isDemoMode.value) { |
| return demoMobileConnectionsRepository.getRepoForSubId(subId) |
| } |
| return realRepository.getRepoForSubId(subId) |
| } |
| } |