blob: ae58398753e823d50c0b739070f181183bf64134 [file] [log] [blame]
/*
* 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)
}
}
}