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