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