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