1 /*
2  * 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 com.android.systemui.R
21 import com.android.systemui.common.shared.model.Text
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dagger.qualifiers.Application
24 import com.android.systemui.qs.tileimpl.QSTileImpl
25 import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
26 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
27 import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
28 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
29 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
30 import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel
31 import com.android.systemui.statusbar.pipeline.shared.ui.model.SignalIcon
32 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
33 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
34 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
35 import javax.inject.Inject
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.ExperimentalCoroutinesApi
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.SharingStarted
40 import kotlinx.coroutines.flow.StateFlow
41 import kotlinx.coroutines.flow.combine
42 import kotlinx.coroutines.flow.flatMapLatest
43 import kotlinx.coroutines.flow.flowOf
44 import kotlinx.coroutines.flow.stateIn
45 
46 /**
47  * View model for the quick settings [InternetTile]. This model exposes mainly a single flow of
48  * InternetTileModel objects, so that updating the tile is as simple as collecting on this state
49  * flow and then calling [QSTileImpl.refreshState]
50  */
51 @OptIn(ExperimentalCoroutinesApi::class)
52 @SysUISingleton
53 class InternetTileViewModel
54 @Inject
55 constructor(
56     airplaneModeRepository: AirplaneModeRepository,
57     connectivityRepository: ConnectivityRepository,
58     ethernetInteractor: EthernetInteractor,
59     mobileIconsInteractor: MobileIconsInteractor,
60     wifiInteractor: WifiInteractor,
61     private val context: Context,
62     @Application scope: CoroutineScope,
63 ) {
64     // Three symmetrical Flows that can be switched upon based on the value of
65     // [DefaultConnectionModel]
66     private val wifiIconFlow: Flow<InternetTileModel> =
67         wifiInteractor.wifiNetwork.flatMapLatest {
68             val wifiIcon = WifiIcon.fromModel(it, context)
69             if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) {
70                 flowOf(
71                     InternetTileModel.Active(
72                         secondaryTitle = removeDoubleQuotes(it.ssid),
73                         icon = ResourceIcon.get(wifiIcon.icon.res)
74                     )
75                 )
76             } else {
77                 notConnectedFlow
78             }
79         }
80 
81     private val mobileDataContentName: Flow<CharSequence?> =
82         mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
83             if (it == null) {
84                 flowOf(null)
85             } else {
86                 combine(it.isRoaming, it.networkTypeIconGroup) { isRoaming, networkTypeIconGroup ->
87                     val cd = loadString(networkTypeIconGroup.contentDescription)
88                     if (isRoaming) {
89                         val roaming = context.getString(R.string.data_connection_roaming)
90                         if (cd != null) {
91                             context.getString(R.string.mobile_data_text_format, roaming, cd)
92                         } else {
93                             roaming
94                         }
95                     } else {
96                         cd
97                     }
98                 }
99             }
100         }
101 
102     private val mobileIconFlow: Flow<InternetTileModel> =
103         mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
104             if (it == null) {
105                 notConnectedFlow
106             } else {
107                 combine(
108                     it.networkName,
109                     it.signalLevelIcon,
110                     mobileDataContentName,
111                 ) { networkNameModel, signalIcon, dataContentDescription ->
112                     InternetTileModel.Active(
113                         secondaryTitle =
114                             mobileDataContentConcat(networkNameModel.name, dataContentDescription),
115                         icon = SignalIcon(signalIcon.toSignalDrawableState()),
116                     )
117                 }
118             }
119         }
120 
121     private fun mobileDataContentConcat(
122         networkName: String?,
123         dataContentDescription: CharSequence?
124     ): String {
125         if (dataContentDescription == null) {
126             return networkName ?: ""
127         }
128         if (networkName == null) {
129             return dataContentDescription.toString()
130         }
131 
132         return context.getString(
133             R.string.mobile_carrier_text_format,
134             networkName,
135             dataContentDescription
136         )
137     }
138 
139     private fun loadString(resId: Int): String? =
140         if (resId > 0) {
141             context.getString(resId)
142         } else {
143             null
144         }
145 
146     private val ethernetIconFlow: Flow<InternetTileModel> =
147         ethernetInteractor.icon.flatMapLatest {
148             if (it == null) {
149                 notConnectedFlow
150             } else {
151                 flowOf(
152                     InternetTileModel.Active(
153                         secondaryTitle = it.contentDescription.toString(),
154                         iconId = it.res
155                     )
156                 )
157             }
158         }
159 
160     private val notConnectedFlow: StateFlow<InternetTileModel> =
161         combine(
162                 wifiInteractor.areNetworksAvailable,
163                 airplaneModeRepository.isAirplaneMode,
164             ) { networksAvailable, isAirplaneMode ->
165                 when {
166                     isAirplaneMode -> {
167                         InternetTileModel.Inactive(
168                             secondaryTitle = context.getString(R.string.status_bar_airplane),
169                             icon = ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable)
170                         )
171                     }
172                     networksAvailable -> {
173                         InternetTileModel.Inactive(
174                             secondaryTitle =
175                                 context.getString(R.string.quick_settings_networks_available),
176                             iconId = R.drawable.ic_qs_no_internet_available,
177                         )
178                     }
179                     else -> {
180                         NOT_CONNECTED_NETWORKS_UNAVAILABLE
181                     }
182                 }
183             }
184             .stateIn(scope, SharingStarted.WhileSubscribed(), NOT_CONNECTED_NETWORKS_UNAVAILABLE)
185 
186     /**
187      * Strict ordering of which repo is sending its data to the internet tile. Swaps between each of
188      * the interim providers (wifi, mobile, ethernet, or not-connected)
189      */
190     private val activeModelProvider: Flow<InternetTileModel> =
191         connectivityRepository.defaultConnections.flatMapLatest {
192             when {
193                 it.ethernet.isDefault -> ethernetIconFlow
194                 it.mobile.isDefault || it.carrierMerged.isDefault -> mobileIconFlow
195                 it.wifi.isDefault -> wifiIconFlow
196                 else -> notConnectedFlow
197             }
198         }
199 
200     /** Consumable flow describing the correct state for the InternetTile */
201     val tileModel: StateFlow<InternetTileModel> =
202         activeModelProvider.stateIn(scope, SharingStarted.WhileSubscribed(), notConnectedFlow.value)
203 
204     companion object {
205         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
206             InternetTileModel.Inactive(
207                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
208                 iconId = R.drawable.ic_qs_no_internet_unavailable,
209             )
210 
211         private fun removeDoubleQuotes(string: String?): String? {
212             if (string == null) return null
213             val length = string.length
214             return if (length > 1 && string[0] == '"' && string[length - 1] == '"') {
215                 string.substring(1, length - 1)
216             } else string
217         }
218     }
219 }
220