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