• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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