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