| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel |
| |
| import android.content.Context |
| import android.text.Html |
| import com.android.systemui.common.shared.model.ContentDescription |
| import com.android.systemui.common.shared.model.Text |
| import com.android.systemui.dagger.SysUISingleton |
| import com.android.systemui.dagger.qualifiers.Application |
| import com.android.systemui.qs.tileimpl.QSTileImpl |
| import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon |
| import com.android.systemui.res.R |
| import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository |
| import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor |
| import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor |
| import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository |
| import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel |
| import com.android.systemui.statusbar.pipeline.shared.ui.model.SignalIcon |
| import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor |
| import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel |
| import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon |
| import javax.inject.Inject |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.ExperimentalCoroutinesApi |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.SharingStarted |
| import kotlinx.coroutines.flow.StateFlow |
| import kotlinx.coroutines.flow.combine |
| import kotlinx.coroutines.flow.flatMapLatest |
| import kotlinx.coroutines.flow.flowOf |
| import kotlinx.coroutines.flow.stateIn |
| |
| /** |
| * View model for the quick settings [InternetTile]. This model exposes mainly a single flow of |
| * InternetTileModel objects, so that updating the tile is as simple as collecting on this state |
| * flow and then calling [QSTileImpl.refreshState] |
| */ |
| @OptIn(ExperimentalCoroutinesApi::class) |
| @SysUISingleton |
| class InternetTileViewModel |
| @Inject |
| constructor( |
| airplaneModeRepository: AirplaneModeRepository, |
| connectivityRepository: ConnectivityRepository, |
| ethernetInteractor: EthernetInteractor, |
| mobileIconsInteractor: MobileIconsInteractor, |
| wifiInteractor: WifiInteractor, |
| private val context: Context, |
| @Application scope: CoroutineScope, |
| ) { |
| private val internetLabel: String = context.getString(R.string.quick_settings_internet_label) |
| |
| // Three symmetrical Flows that can be switched upon based on the value of |
| // [DefaultConnectionModel] |
| private val wifiIconFlow: Flow<InternetTileModel> = |
| wifiInteractor.wifiNetwork.flatMapLatest { |
| val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true) |
| if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) { |
| val secondary = removeDoubleQuotes(it.ssid) |
| flowOf( |
| InternetTileModel.Active( |
| secondaryTitle = secondary, |
| icon = ResourceIcon.get(wifiIcon.icon.res), |
| stateDescription = wifiIcon.contentDescription, |
| contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), |
| ) |
| ) |
| } else { |
| notConnectedFlow |
| } |
| } |
| |
| private val mobileDataContentName: Flow<CharSequence?> = |
| mobileIconsInteractor.activeDataIconInteractor.flatMapLatest { |
| if (it == null) { |
| flowOf(null) |
| } else { |
| combine(it.isRoaming, it.networkTypeIconGroup) { isRoaming, networkTypeIconGroup -> |
| val cd = loadString(networkTypeIconGroup.contentDescription) |
| if (isRoaming) { |
| val roaming = context.getString(R.string.data_connection_roaming) |
| if (cd != null) { |
| context.getString(R.string.mobile_data_text_format, roaming, cd) |
| } else { |
| roaming |
| } |
| } else { |
| cd |
| } |
| } |
| } |
| } |
| |
| private val mobileIconFlow: Flow<InternetTileModel> = |
| mobileIconsInteractor.activeDataIconInteractor.flatMapLatest { |
| if (it == null) { |
| notConnectedFlow |
| } else { |
| combine( |
| it.networkName, |
| it.signalLevelIcon, |
| mobileDataContentName, |
| ) { networkNameModel, signalIcon, dataContentDescription -> |
| val secondary = |
| mobileDataContentConcat(networkNameModel.name, dataContentDescription) |
| InternetTileModel.Active( |
| secondaryTitle = secondary, |
| icon = SignalIcon(signalIcon.toSignalDrawableState()), |
| stateDescription = ContentDescription.Loaded(secondary.toString()), |
| contentDescription = ContentDescription.Loaded(internetLabel), |
| ) |
| } |
| } |
| } |
| |
| private fun mobileDataContentConcat( |
| networkName: String?, |
| dataContentDescription: CharSequence? |
| ): CharSequence { |
| if (dataContentDescription == null) { |
| return networkName ?: "" |
| } |
| if (networkName == null) { |
| return Html.fromHtml(dataContentDescription.toString(), 0) |
| } |
| |
| return Html.fromHtml( |
| context.getString( |
| R.string.mobile_carrier_text_format, |
| networkName, |
| dataContentDescription |
| ), |
| 0 |
| ) |
| } |
| |
| private fun loadString(resId: Int): CharSequence? = |
| if (resId > 0) { |
| context.getString(resId) |
| } else { |
| null |
| } |
| |
| private val ethernetIconFlow: Flow<InternetTileModel> = |
| ethernetInteractor.icon.flatMapLatest { |
| if (it == null) { |
| notConnectedFlow |
| } else { |
| val secondary = it.contentDescription |
| flowOf( |
| InternetTileModel.Active( |
| secondaryLabel = secondary?.toText(), |
| iconId = it.res, |
| stateDescription = null, |
| contentDescription = secondary, |
| ) |
| ) |
| } |
| } |
| |
| private val notConnectedFlow: StateFlow<InternetTileModel> = |
| combine( |
| wifiInteractor.areNetworksAvailable, |
| airplaneModeRepository.isAirplaneMode, |
| ) { networksAvailable, isAirplaneMode -> |
| when { |
| isAirplaneMode -> { |
| val secondary = context.getString(R.string.status_bar_airplane) |
| InternetTileModel.Inactive( |
| secondaryTitle = secondary, |
| icon = ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable), |
| stateDescription = null, |
| contentDescription = ContentDescription.Loaded(secondary), |
| ) |
| } |
| networksAvailable -> { |
| val secondary = |
| context.getString(R.string.quick_settings_networks_available) |
| InternetTileModel.Inactive( |
| secondaryTitle = secondary, |
| iconId = R.drawable.ic_qs_no_internet_available, |
| stateDescription = null, |
| contentDescription = |
| ContentDescription.Loaded("$internetLabel,$secondary") |
| ) |
| } |
| else -> { |
| NOT_CONNECTED_NETWORKS_UNAVAILABLE |
| } |
| } |
| } |
| .stateIn(scope, SharingStarted.WhileSubscribed(), NOT_CONNECTED_NETWORKS_UNAVAILABLE) |
| |
| /** |
| * Strict ordering of which repo is sending its data to the internet tile. Swaps between each of |
| * the interim providers (wifi, mobile, ethernet, or not-connected) |
| */ |
| private val activeModelProvider: Flow<InternetTileModel> = |
| connectivityRepository.defaultConnections.flatMapLatest { |
| when { |
| it.ethernet.isDefault -> ethernetIconFlow |
| it.mobile.isDefault || it.carrierMerged.isDefault -> mobileIconFlow |
| it.wifi.isDefault -> wifiIconFlow |
| else -> notConnectedFlow |
| } |
| } |
| |
| /** Consumable flow describing the correct state for the InternetTile */ |
| val tileModel: StateFlow<InternetTileModel> = |
| activeModelProvider.stateIn(scope, SharingStarted.WhileSubscribed(), notConnectedFlow.value) |
| |
| companion object { |
| val NOT_CONNECTED_NETWORKS_UNAVAILABLE = |
| InternetTileModel.Inactive( |
| secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), |
| iconId = R.drawable.ic_qs_no_internet_unavailable, |
| stateDescription = null, |
| contentDescription = |
| ContentDescription.Resource(R.string.quick_settings_networks_unavailable), |
| ) |
| |
| private fun removeDoubleQuotes(string: String?): String? { |
| if (string == null) return null |
| val length = string.length |
| return if (length > 1 && string[0] == '"' && string[length - 1] == '"') { |
| string.substring(1, length - 1) |
| } else string |
| } |
| |
| private fun ContentDescription.toText(): Text = |
| when (this) { |
| is ContentDescription.Loaded -> Text.Loaded(this.description) |
| is ContentDescription.Resource -> Text.Resource(this.res) |
| } |
| } |
| } |