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