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 com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
20 import com.android.systemui.common.shared.model.ContentDescription
21 import com.android.systemui.common.shared.model.Icon
22 import com.android.systemui.log.table.logDiffsForTable
23 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
24 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
25 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
26 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
27 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
28 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
29 import kotlinx.coroutines.CoroutineScope
30 import kotlinx.coroutines.ExperimentalCoroutinesApi
31 import kotlinx.coroutines.flow.Flow
32 import kotlinx.coroutines.flow.SharingStarted
33 import kotlinx.coroutines.flow.StateFlow
34 import kotlinx.coroutines.flow.combine
35 import kotlinx.coroutines.flow.distinctUntilChanged
36 import kotlinx.coroutines.flow.flowOf
37 import kotlinx.coroutines.flow.map
38 import kotlinx.coroutines.flow.stateIn
39 
40 /** Common interface for all of the location-based mobile icon view models. */
41 interface MobileIconViewModelCommon {
42     val subscriptionId: Int
43     /** True if this view should be visible at all. */
44     val isVisible: StateFlow<Boolean>
45     val icon: Flow<SignalIconModel>
46     val contentDescription: Flow<ContentDescription>
47     val roaming: Flow<Boolean>
48     /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
49     val networkTypeIcon: Flow<Icon.Resource?>
50     val activityInVisible: Flow<Boolean>
51     val activityOutVisible: Flow<Boolean>
52     val activityContainerVisible: Flow<Boolean>
53 }
54 
55 /**
56  * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
57  * a single line of service via [MobileIconInteractor] and update the UI based on that
58  * subscription's information.
59  *
60  * There will be exactly one [MobileIconViewModel] per filtered subscription offered from
61  * [MobileIconsInteractor.filteredSubscriptions].
62  *
63  * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon]
64  * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view
65  * model gets the exact same information, as well as allows us to log that unified state only once
66  * per icon.
67  */
68 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
69 @OptIn(ExperimentalCoroutinesApi::class)
70 class MobileIconViewModel
71 constructor(
72     override val subscriptionId: Int,
73     iconInteractor: MobileIconInteractor,
74     airplaneModeInteractor: AirplaneModeInteractor,
75     constants: ConnectivityConstants,
76     scope: CoroutineScope,
77 ) : MobileIconViewModelCommon {
78     override val isVisible: StateFlow<Boolean> =
79         if (!constants.hasDataCapabilities) {
80                 flowOf(false)
81             } else {
82                 combine(
83                     airplaneModeInteractor.isAirplaneMode,
84                     iconInteractor.isAllowedDuringAirplaneMode,
85                     iconInteractor.isForceHidden,
86                 ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden ->
87                     if (isForceHidden) {
88                         false
89                     } else if (isAirplaneMode) {
90                         isAllowedDuringAirplaneMode
91                     } else {
92                         true
93                     }
94                 }
95             }
96             .distinctUntilChanged()
97             .logDiffsForTable(
98                 iconInteractor.tableLogBuffer,
99                 columnPrefix = "",
100                 columnName = "visible",
101                 initialValue = false,
102             )
103             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
104 
105     override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon
106 
107     override val contentDescription: Flow<ContentDescription> = run {
108         val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH[0])
109         iconInteractor.signalLevelIcon
110             .map { ContentDescription.Resource(PHONE_SIGNAL_STRENGTH[it.level]) }
111             .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
112     }
113 
114     private val showNetworkTypeIcon: Flow<Boolean> =
115         combine(
116                 iconInteractor.isDataConnected,
117                 iconInteractor.isDataEnabled,
118                 iconInteractor.alwaysShowDataRatIcon,
119                 iconInteractor.mobileIsDefault,
120                 iconInteractor.carrierNetworkChangeActive,
121             ) { dataConnected, dataEnabled, alwaysShow, mobileIsDefault, carrierNetworkChange ->
122                 alwaysShow ||
123                     (!carrierNetworkChange && (dataEnabled && dataConnected && mobileIsDefault))
124             }
125             .distinctUntilChanged()
126             .logDiffsForTable(
127                 iconInteractor.tableLogBuffer,
128                 columnPrefix = "",
129                 columnName = "showNetworkTypeIcon",
130                 initialValue = false,
131             )
132             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
133 
134     override val networkTypeIcon: Flow<Icon.Resource?> =
135         combine(
136                 iconInteractor.networkTypeIconGroup,
137                 showNetworkTypeIcon,
138             ) { networkTypeIconGroup, shouldShow ->
139                 val desc =
140                     if (networkTypeIconGroup.contentDescription != 0)
141                         ContentDescription.Resource(networkTypeIconGroup.contentDescription)
142                     else null
143                 val icon =
144                     if (networkTypeIconGroup.iconId != 0)
145                         Icon.Resource(networkTypeIconGroup.iconId, desc)
146                     else null
147                 return@combine when {
148                     !shouldShow -> null
149                     else -> icon
150                 }
151             }
152             .distinctUntilChanged()
153             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
154 
155     override val roaming: StateFlow<Boolean> =
156         iconInteractor.isRoaming
157             .logDiffsForTable(
158                 iconInteractor.tableLogBuffer,
159                 columnPrefix = "",
160                 columnName = "roaming",
161                 initialValue = false,
162             )
163             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
164 
165     private val activity: Flow<DataActivityModel?> =
166         if (!constants.shouldShowActivityConfig) {
167             flowOf(null)
168         } else {
169             iconInteractor.activity
170         }
171 
172     override val activityInVisible: Flow<Boolean> =
173         activity
174             .map { it?.hasActivityIn ?: false }
175             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
176 
177     override val activityOutVisible: Flow<Boolean> =
178         activity
179             .map { it?.hasActivityOut ?: false }
180             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
181 
182     override val activityContainerVisible: Flow<Boolean> =
183         activity
184             .map { it != null && (it.hasActivityIn || it.hasActivityOut) }
185             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
186 }
187