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.ui.viewmodel 18 19 import androidx.annotation.VisibleForTesting 20 import com.android.systemui.dagger.SysUISingleton 21 import com.android.systemui.dagger.qualifiers.Application 22 import com.android.systemui.statusbar.phone.StatusBarLocation 23 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags 24 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor 25 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor 26 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor 27 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger 28 import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger 29 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView 30 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants 31 import javax.inject.Inject 32 import kotlinx.coroutines.CoroutineScope 33 import kotlinx.coroutines.ExperimentalCoroutinesApi 34 import kotlinx.coroutines.flow.SharingStarted 35 import kotlinx.coroutines.flow.StateFlow 36 import kotlinx.coroutines.flow.flatMapLatest 37 import kotlinx.coroutines.flow.flowOf 38 import kotlinx.coroutines.flow.map 39 import kotlinx.coroutines.flow.mapLatest 40 import kotlinx.coroutines.flow.stateIn 41 import kotlinx.coroutines.launch 42 43 /** 44 * View model for describing the system's current mobile cellular connections. The result is a list 45 * of [MobileIconViewModel]s which describe the individual icons and can be bound to 46 * [ModernStatusBarMobileView]. 47 */ 48 @OptIn(ExperimentalCoroutinesApi::class) 49 @SysUISingleton 50 class MobileIconsViewModel 51 @Inject 52 constructor( 53 val logger: MobileViewLogger, 54 private val verboseLogger: VerboseMobileViewLogger, 55 private val interactor: MobileIconsInteractor, 56 private val airplaneModeInteractor: AirplaneModeInteractor, 57 private val constants: ConnectivityConstants, 58 @Application private val scope: CoroutineScope, 59 private val statusBarPipelineFlags: StatusBarPipelineFlags, 60 ) { 61 @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>() 62 @VisibleForTesting 63 val mobileIconInteractorSubIdCache = mutableMapOf<Int, MobileIconInteractor>() 64 65 val subscriptionIdsFlow: StateFlow<List<Int>> = 66 interactor.filteredSubscriptions 67 .mapLatest { subscriptions -> 68 subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } 69 } 70 .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) 71 72 private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = 73 subscriptionIdsFlow 74 .map { 75 if (it.isEmpty()) { 76 null 77 } else { 78 // Mobile icons get reversed by [StatusBarIconController], so the last element 79 // in this list will show up visually first. 80 commonViewModelForSub(it.last()) 81 } 82 } 83 .stateIn(scope, SharingStarted.WhileSubscribed(), null) 84 85 /** 86 * A flow that emits `true` if the mobile sub that's displayed first visually is showing its 87 * network type icon and `false` otherwise. 88 */ 89 val firstMobileSubShowingNetworkTypeIcon: StateFlow<Boolean> = 90 firstMobileSubViewModel 91 .flatMapLatest { firstMobileSubViewModel -> 92 firstMobileSubViewModel?.networkTypeIcon?.map { it != null } ?: flowOf(false) 93 } 94 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 95 96 init { 97 scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } 98 } 99 100 fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { 101 val common = commonViewModelForSub(subId) 102 return LocationBasedMobileViewModel.viewModelForLocation( 103 common, 104 interactor.getMobileConnectionInteractorForSubId(subId), 105 verboseLogger, 106 location, 107 scope, 108 ) 109 } 110 111 private fun commonViewModelForSub(subId: Int): MobileIconViewModelCommon { 112 return mobileIconSubIdCache[subId] 113 ?: MobileIconViewModel( 114 subId, 115 interactor.getMobileConnectionInteractorForSubId(subId), 116 airplaneModeInteractor, 117 constants, 118 scope, 119 ) 120 .also { mobileIconSubIdCache[subId] = it } 121 } 122 123 private fun invalidateCaches(subIds: List<Int>) { 124 val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) } 125 subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) } 126 127 mobileIconInteractorSubIdCache.keys 128 .filter { !subIds.contains(it) } 129 .forEach { subId -> mobileIconInteractorSubIdCache.remove(subId) } 130 } 131 } 132