1 /* <lambda>null2 * Copyright (C) 2023 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.shared.ui.viewmodel 18 19 import android.content.Context 20 import android.text.Html 21 import com.android.systemui.common.shared.model.ContentDescription 22 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription 23 import com.android.systemui.common.shared.model.Text 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dagger.qualifiers.Application 26 import com.android.systemui.qs.tileimpl.QSTileImpl 27 import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon 28 import com.android.systemui.res.R 29 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository 30 import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor 31 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor 32 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel 33 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository 34 import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel 35 import com.android.systemui.statusbar.pipeline.shared.ui.model.SignalIcon 36 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor 37 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel 38 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon 39 import javax.inject.Inject 40 import kotlinx.coroutines.CoroutineScope 41 import kotlinx.coroutines.ExperimentalCoroutinesApi 42 import kotlinx.coroutines.flow.Flow 43 import kotlinx.coroutines.flow.SharingStarted 44 import kotlinx.coroutines.flow.StateFlow 45 import kotlinx.coroutines.flow.combine 46 import kotlinx.coroutines.flow.flatMapLatest 47 import kotlinx.coroutines.flow.flowOf 48 import kotlinx.coroutines.flow.stateIn 49 50 /** 51 * View model for the quick settings [InternetTile]. This model exposes mainly a single flow of 52 * InternetTileModel objects, so that updating the tile is as simple as collecting on this state 53 * flow and then calling [QSTileImpl.refreshState] 54 */ 55 @OptIn(ExperimentalCoroutinesApi::class) 56 @SysUISingleton 57 class InternetTileViewModel 58 @Inject 59 constructor( 60 airplaneModeRepository: AirplaneModeRepository, 61 connectivityRepository: ConnectivityRepository, 62 ethernetInteractor: EthernetInteractor, 63 mobileIconsInteractor: MobileIconsInteractor, 64 wifiInteractor: WifiInteractor, 65 private val context: Context, 66 @Application scope: CoroutineScope, 67 ) { 68 private val internetLabel: String = context.getString(R.string.quick_settings_internet_label) 69 70 // Three symmetrical Flows that can be switched upon based on the value of 71 // [DefaultConnectionModel] 72 private val wifiIconFlow: Flow<InternetTileModel> = 73 wifiInteractor.wifiNetwork.flatMapLatest { 74 val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true) 75 if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) { 76 val secondary = removeDoubleQuotes(it.ssid) 77 flowOf( 78 InternetTileModel.Active( 79 secondaryTitle = secondary, 80 icon = ResourceIcon.get(wifiIcon.icon.res), 81 stateDescription = wifiIcon.contentDescription, 82 contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), 83 ) 84 ) 85 } else { 86 notConnectedFlow 87 } 88 } 89 90 private val mobileDataContentName: Flow<CharSequence?> = 91 mobileIconsInteractor.activeDataIconInteractor.flatMapLatest { 92 if (it == null) { 93 flowOf(null) 94 } else { 95 combine(it.isRoaming, it.networkTypeIconGroup) { isRoaming, networkTypeIconGroup -> 96 val cd = loadString(networkTypeIconGroup.contentDescription) 97 if (isRoaming) { 98 val roaming = context.getString(R.string.data_connection_roaming) 99 if (cd != null) { 100 context.getString(R.string.mobile_data_text_format, roaming, cd) 101 } else { 102 roaming 103 } 104 } else { 105 cd 106 } 107 } 108 } 109 } 110 111 private val mobileIconFlow: Flow<InternetTileModel> = 112 mobileIconsInteractor.activeDataIconInteractor.flatMapLatest { 113 if (it == null) { 114 notConnectedFlow 115 } else { 116 combine( 117 it.networkName, 118 it.signalLevelIcon, 119 mobileDataContentName, 120 ) { networkNameModel, signalIcon, dataContentDescription -> 121 when (signalIcon) { 122 is SignalIconModel.Cellular -> { 123 val secondary = 124 mobileDataContentConcat( 125 networkNameModel.name, 126 dataContentDescription 127 ) 128 InternetTileModel.Active( 129 secondaryTitle = secondary, 130 icon = SignalIcon(signalIcon.toSignalDrawableState()), 131 stateDescription = ContentDescription.Loaded(secondary.toString()), 132 contentDescription = ContentDescription.Loaded(internetLabel), 133 ) 134 } 135 is SignalIconModel.Satellite -> { 136 val secondary = 137 signalIcon.icon.contentDescription.loadContentDescription(context) 138 InternetTileModel.Active( 139 secondaryTitle = secondary, 140 iconId = signalIcon.icon.res, 141 stateDescription = ContentDescription.Loaded(secondary), 142 contentDescription = ContentDescription.Loaded(internetLabel), 143 ) 144 } 145 } 146 } 147 } 148 } 149 150 private fun mobileDataContentConcat( 151 networkName: String?, 152 dataContentDescription: CharSequence? 153 ): CharSequence { 154 if (dataContentDescription == null) { 155 return networkName ?: "" 156 } 157 if (networkName == null) { 158 return Html.fromHtml(dataContentDescription.toString(), 0) 159 } 160 161 return Html.fromHtml( 162 context.getString( 163 R.string.mobile_carrier_text_format, 164 networkName, 165 dataContentDescription 166 ), 167 0 168 ) 169 } 170 171 private fun loadString(resId: Int): CharSequence? = 172 if (resId > 0) { 173 context.getString(resId) 174 } else { 175 null 176 } 177 178 private val ethernetIconFlow: Flow<InternetTileModel> = 179 ethernetInteractor.icon.flatMapLatest { 180 if (it == null) { 181 notConnectedFlow 182 } else { 183 val secondary = it.contentDescription 184 flowOf( 185 InternetTileModel.Active( 186 secondaryLabel = secondary?.toText(), 187 iconId = it.res, 188 stateDescription = null, 189 contentDescription = secondary, 190 ) 191 ) 192 } 193 } 194 195 private val notConnectedFlow: StateFlow<InternetTileModel> = 196 combine( 197 wifiInteractor.areNetworksAvailable, 198 airplaneModeRepository.isAirplaneMode, 199 ) { networksAvailable, isAirplaneMode -> 200 when { 201 isAirplaneMode -> { 202 val secondary = context.getString(R.string.status_bar_airplane) 203 InternetTileModel.Inactive( 204 secondaryTitle = secondary, 205 icon = ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable), 206 stateDescription = null, 207 contentDescription = ContentDescription.Loaded(secondary), 208 ) 209 } 210 networksAvailable -> { 211 val secondary = 212 context.getString(R.string.quick_settings_networks_available) 213 InternetTileModel.Inactive( 214 secondaryTitle = secondary, 215 iconId = R.drawable.ic_qs_no_internet_available, 216 stateDescription = null, 217 contentDescription = 218 ContentDescription.Loaded("$internetLabel,$secondary") 219 ) 220 } 221 else -> { 222 NOT_CONNECTED_NETWORKS_UNAVAILABLE 223 } 224 } 225 } 226 .stateIn(scope, SharingStarted.WhileSubscribed(), NOT_CONNECTED_NETWORKS_UNAVAILABLE) 227 228 /** 229 * Strict ordering of which repo is sending its data to the internet tile. Swaps between each of 230 * the interim providers (wifi, mobile, ethernet, or not-connected) 231 */ 232 private val activeModelProvider: Flow<InternetTileModel> = 233 connectivityRepository.defaultConnections.flatMapLatest { 234 when { 235 it.ethernet.isDefault -> ethernetIconFlow 236 it.mobile.isDefault || it.carrierMerged.isDefault -> mobileIconFlow 237 it.wifi.isDefault -> wifiIconFlow 238 else -> notConnectedFlow 239 } 240 } 241 242 /** Consumable flow describing the correct state for the InternetTile */ 243 val tileModel: StateFlow<InternetTileModel> = 244 activeModelProvider.stateIn(scope, SharingStarted.WhileSubscribed(), notConnectedFlow.value) 245 246 companion object { 247 val NOT_CONNECTED_NETWORKS_UNAVAILABLE = 248 InternetTileModel.Inactive( 249 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), 250 iconId = R.drawable.ic_qs_no_internet_unavailable, 251 stateDescription = null, 252 contentDescription = 253 ContentDescription.Resource(R.string.quick_settings_networks_unavailable), 254 ) 255 256 private fun removeDoubleQuotes(string: String?): String? { 257 if (string == null) return null 258 val length = string.length 259 return if (length > 1 && string[0] == '"' && string[length - 1] == '"') { 260 string.substring(1, length - 1) 261 } else string 262 } 263 264 private fun ContentDescription.toText(): Text = 265 when (this) { 266 is ContentDescription.Loaded -> Text.Loaded(this.description) 267 is ContentDescription.Resource -> Text.Resource(this.res) 268 } 269 } 270 } 271