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