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