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.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.flags.FeatureFlagsClassic 23 import com.android.systemui.statusbar.phone.StatusBarLocation 24 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor 25 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor 26 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger 27 import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger 28 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView 29 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants 30 import javax.inject.Inject 31 import kotlinx.coroutines.CoroutineScope 32 import kotlinx.coroutines.ExperimentalCoroutinesApi 33 import kotlinx.coroutines.Job 34 import kotlinx.coroutines.cancel 35 import kotlinx.coroutines.flow.SharingStarted 36 import kotlinx.coroutines.flow.StateFlow 37 import kotlinx.coroutines.flow.flatMapLatest 38 import kotlinx.coroutines.flow.flowOf 39 import kotlinx.coroutines.flow.map 40 import kotlinx.coroutines.flow.mapLatest 41 import kotlinx.coroutines.flow.stateIn 42 import kotlinx.coroutines.launch 43 44 /** 45 * View model for describing the system's current mobile cellular connections. The result is a list 46 * of [MobileIconViewModel]s which describe the individual icons and can be bound to 47 * [ModernStatusBarMobileView]. 48 */ 49 @OptIn(ExperimentalCoroutinesApi::class) 50 @SysUISingleton 51 class MobileIconsViewModel 52 @Inject 53 constructor( 54 val logger: MobileViewLogger, 55 private val verboseLogger: VerboseMobileViewLogger, 56 private val interactor: MobileIconsInteractor, 57 private val airplaneModeInteractor: AirplaneModeInteractor, 58 private val constants: ConnectivityConstants, 59 private val flags: FeatureFlagsClassic, 60 @Application private val scope: CoroutineScope, 61 ) { 62 @VisibleForTesting 63 val reuseCache = mutableMapOf<Int, Pair<MobileIconViewModel, CoroutineScope>>() 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 reuseCache.getOrPut(subId) { createViewModel(subId) }.first 113 } 114 115 private fun createViewModel(subId: Int): Pair<MobileIconViewModel, CoroutineScope> { 116 // Create a child scope so we can cancel it 117 val vmScope = scope.createChildScope() 118 val vm = 119 MobileIconViewModel( 120 subId, 121 interactor.getMobileConnectionInteractorForSubId(subId), 122 airplaneModeInteractor, 123 constants, 124 flags, 125 vmScope, 126 ) 127 128 return Pair(vm, vmScope) 129 } 130 131 private fun CoroutineScope.createChildScope() = 132 CoroutineScope(coroutineContext + Job(coroutineContext[Job])) 133 134 private fun invalidateCaches(subIds: List<Int>) { 135 reuseCache.keys 136 .filter { !subIds.contains(it) } 137 .forEach { id -> 138 reuseCache 139 .remove(id) 140 // Cancel the view model's scope after removing it 141 ?.second 142 ?.cancel() 143 } 144 } 145 } 146