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