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