diff options
17 files changed, 247 insertions, 133 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 4b3ebc2bd53d..da54cb8e4679 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.setProgress import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp @@ -106,7 +107,10 @@ fun VolumeSlider( return } - Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) { + Column( + modifier = modifier.animateContentSize().semantics(true) {}, + verticalArrangement = Arrangement.Top, + ) { Row( horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth().height(40.dp), @@ -123,7 +127,7 @@ fun VolumeSlider( text = state.label, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).clearAndSetSemantics {}, ) button?.invoke() } @@ -134,12 +138,11 @@ fun VolumeSlider( onValueChanged = onValueChange, onValueChangeFinished = { onValueChangeFinished?.invoke() }, isEnabled = state.isEnabled, - stepDistance = state.a11yStep, + stepDistance = state.step, accessibilityParams = AccessibilityParams( - label = state.label, - disabledMessage = state.disabledMessage, - currentStateDescription = state.a11yStateDescription, + contentDescription = state.a11yContentDescription, + stateDescription = state.a11yStateDescription, ), haptics = hapticsViewModelFactory?.let { @@ -169,7 +172,7 @@ fun VolumeSlider( text = disabledMessage, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelSmall, - modifier = Modifier.basicMarquee(), + modifier = Modifier.basicMarquee().clearAndSetSemantics {}, ) } } @@ -229,7 +232,7 @@ private fun LegacyVolumeSlider( } val newValue = - (value + targetDirection * state.a11yStep).coerceIn( + (value + targetDirection * state.step).coerceIn( state.valueRange.start, state.valueRange.endInclusive, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt index 04ab98889755..b1a3caf98f09 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.bluetooth.BluetoothDevice +import android.graphics.drawable.TestStubDrawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.bluetooth.CachedBluetoothDevice @@ -63,6 +64,12 @@ class AudioSharingStreamSliderViewModelTest : SysuiTestCase() { assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2") assertThat(audioSharingSlider!!.icon) - .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null)) + .isEqualTo( + Icon.Loaded( + drawable = TestStubDrawable(), + res = R.drawable.ic_volume_media_bt, + contentDescription = null, + ) + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt index 9e8cde3bc936..ffe8e923815f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.app.Flags import android.app.NotificationManager.INTERRUPTION_FILTER_NONE +import android.graphics.drawable.TestStubDrawable import android.media.AudioManager import android.platform.test.annotations.EnableFlags import android.service.notification.ZenPolicy @@ -28,7 +29,6 @@ import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.collectLastValue @@ -39,8 +39,6 @@ import com.android.systemui.statusbar.policy.data.repository.fakeZenModeReposito import com.android.systemui.testKosmos import com.android.systemui.volume.data.repository.audioSharingRepository import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -173,6 +171,12 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() { assertThat(mediaSlider!!.label).isEqualTo("my headset 1") assertThat(mediaSlider!!.icon) - .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null)) + .isEqualTo( + Icon.Loaded( + drawable = TestStubDrawable(), + res = R.drawable.ic_volume_media_bt, + contentDescription = null, + ) + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt index 2adaec21867f..6792f3188986 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt @@ -19,6 +19,7 @@ package com.android.systemui.common.shared.model import android.annotation.DrawableRes import android.graphics.drawable.Drawable import androidx.compose.runtime.Stable +import com.android.systemui.common.shared.model.Icon.Loaded /** * Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference] @@ -33,8 +34,37 @@ sealed class Icon { constructor( val drawable: Drawable, override val contentDescription: ContentDescription?, + /** + * Serves as an id to compare two instances. When provided this is used alongside + * [contentDescription] to determine equality. This is useful when comparing icons + * representing the same UI, but with different [drawable] instances. + */ @DrawableRes val res: Int? = null, - ) : Icon() + ) : Icon() { + + override fun equals(other: Any?): Boolean { + val that = other as? Loaded ?: return false + + if (this.res != null && that.res != null) { + return this.res == that.res && this.contentDescription == that.contentDescription + } + + return this.res == that.res && + this.drawable == that.drawable && + this.contentDescription == that.contentDescription + } + + override fun hashCode(): Int { + var result = contentDescription?.hashCode() ?: 0 + result = + if (res != null) { + 31 * result + res.hashCode() + } else { + 31 * result + drawable.hashCode() + } + return result + } + } data class Resource( @DrawableRes val res: Int, @@ -49,4 +79,4 @@ sealed class Icon { fun Drawable.asIcon( contentDescription: ContentDescription? = null, @DrawableRes res: Int? = null, -): Icon.Loaded = Icon.Loaded(this, contentDescription, res) +): Loaded = Loaded(this, contentDescription, res) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index 43d1ef478ae1..32f784f17bb7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -16,7 +16,6 @@ package com.android.systemui.volume.dialog.sliders.ui -import android.graphics.drawable.Drawable import android.view.View import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -29,7 +28,6 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable @@ -43,7 +41,8 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme -import com.android.compose.ui.graphics.painter.DrawablePainter +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.compose.Icon import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R @@ -155,7 +154,7 @@ private fun VolumeDialogSlider( }, ) }, - accessibilityParams = AccessibilityParams(label = sliderStateModel.label), + accessibilityParams = AccessibilityParams(contentDescription = sliderStateModel.label), modifier = modifier.pointerInput(Unit) { coroutineScope { @@ -172,7 +171,7 @@ private fun VolumeDialogSlider( @Composable private fun BoxScope.VolumeIcon( - drawable: Drawable, + icon: Icon.Loaded, isVisible: Boolean, modifier: Modifier = Modifier, ) { @@ -182,6 +181,6 @@ private fun BoxScope.VolumeIcon( exit = fadeOut(animationSpec = tween(durationMillis = 50)), modifier = modifier.align(Alignment.Center).size(40.dp).padding(10.dp), ) { - Icon(painter = DrawablePainter(drawable), contentDescription = null) + Icon(icon) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt index ef147c741bec..3712276488ff 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt @@ -18,32 +18,36 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import android.annotation.SuppressLint import android.content.Context -import android.graphics.drawable.Drawable import android.media.AudioManager import androidx.annotation.DrawableRes import com.android.settingslib.R as SettingsR import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.RingerMode +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.withContext @SuppressLint("UseCompatLoadingForDrawables") class VolumeDialogSliderIconProvider @Inject constructor( private val context: Context, + @UiBackground private val uiBackgroundContext: CoroutineContext, private val zenModeInteractor: ZenModeInteractor, private val audioVolumeInteractor: AudioVolumeInteractor, ) { - fun getAudioSharingIcon(isMuted: Boolean): Flow<Drawable> { + fun getAudioSharingIcon(isMuted: Boolean): Flow<Icon.Loaded> { return flow { val iconRes = if (isMuted) { @@ -51,11 +55,12 @@ constructor( } else { R.drawable.ic_volume_media_bt } - emit(context.getDrawable(iconRes)!!) + val drawable = withContext(uiBackgroundContext) { context.getDrawable(iconRes)!! } + emit(Icon.Loaded(drawable = drawable, contentDescription = null, res = iconRes)) } } - fun getCastIcon(isMuted: Boolean): Flow<Drawable> { + fun getCastIcon(isMuted: Boolean): Flow<Icon.Loaded> { return flow { val iconRes = if (isMuted) { @@ -63,7 +68,8 @@ constructor( } else { SettingsR.drawable.ic_volume_remote } - emit(context.getDrawable(iconRes)!!) + val drawable = withContext(uiBackgroundContext) { context.getDrawable(iconRes)!! } + emit(Icon.Loaded(drawable = drawable, contentDescription = null, res = iconRes)) } } @@ -74,15 +80,18 @@ constructor( levelMax: Int, isMuted: Boolean, isRoutedToBluetooth: Boolean, - ): Flow<Drawable> { + ): Flow<Icon.Loaded> { return combine( zenModeInteractor.activeModesBlockingStream(stream), ringerModeForStream(stream), ) { activeModesBlockingStream, ringerMode -> if (activeModesBlockingStream?.mainMode?.icon != null) { - return@combine activeModesBlockingStream.mainMode.icon.drawable + Icon.Loaded( + drawable = activeModesBlockingStream.mainMode.icon.drawable, + contentDescription = null, + ) } else { - context.getDrawable( + val iconRes = getIconRes( stream, level, @@ -92,7 +101,8 @@ constructor( isRoutedToBluetooth, ringerMode, ) - )!! + val drawable = withContext(uiBackgroundContext) { context.getDrawable(iconRes)!! } + Icon.Loaded(drawable = drawable, contentDescription = null, res = iconRes) } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt index 88a061f3813c..ed59598d97d0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import android.content.Context -import android.graphics.drawable.Drawable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel import com.android.systemui.volume.dialog.shared.model.streamLabel @@ -25,14 +25,14 @@ data class VolumeDialogSliderStateModel( val value: Float, val isDisabled: Boolean, val valueRange: ClosedFloatingPointRange<Float>, - val icon: Drawable, + val icon: Icon.Loaded, val label: String, ) fun VolumeDialogStreamModel.toStateModel( context: Context, isDisabled: Boolean, - icon: Drawable, + icon: Icon.Loaded, ): VolumeDialogSliderStateModel { return VolumeDialogSliderStateModel( value = level.toFloat(), diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt index f6aa189eb571..faf0abd4cabd 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt @@ -108,11 +108,9 @@ constructor( isMuted = isMuted, isRoutedToBluetooth = routedToBluetooth, ) - is VolumeDialogSliderType.RemoteMediaStream -> { volumeDialogSliderIconProvider.getCastIcon(isMuted) } - is VolumeDialogSliderType.AudioSharingStream -> { volumeDialogSliderIconProvider.getAudioSharingIcon(isMuted) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt index 3d98ebacc7ca..f6452679cbf4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt @@ -16,9 +16,11 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel +import android.content.Context import com.android.internal.logging.UiEventLogger import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R @@ -28,6 +30,7 @@ import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -39,11 +42,14 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext class AudioSharingStreamSliderViewModel @AssistedInject constructor( + private val context: Context, @Assisted private val coroutineScope: CoroutineScope, + @UiBackground private val uiBackgroundContext: CoroutineContext, private val audioSharingInteractor: AudioSharingInteractor, private val uiEventLogger: UiEventLogger, private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, @@ -51,6 +57,12 @@ constructor( ) : SliderViewModel { private val volumeChanges = MutableStateFlow<Int?>(null) + private val audioSharingIcon = + Icon.Loaded( + drawable = context.getDrawable(R.drawable.ic_volume_media_bt)!!, + contentDescription = null, + res = R.drawable.ic_volume_media_bt, + ) override val slider: StateFlow<SliderState> = combine( audioSharingInteractor.volume.distinctUntilChanged().onEach { @@ -62,16 +74,17 @@ constructor( if (volume == null) { SliderState.Empty } else { - - State( - value = volume.toFloat(), - valueRange = - audioSharingInteractor.volumeMin.toFloat()..audioSharingInteractor - .volumeMax - .toFloat(), - icon = Icon.Resource(R.drawable.ic_volume_media_bt, null), - label = deviceName, - ) + withContext(uiBackgroundContext) { + State( + value = volume.toFloat(), + valueRange = + audioSharingInteractor.volumeMin.toFloat()..audioSharingInteractor + .volumeMax + .toFloat(), + icon = audioSharingIcon, + label = deviceName, + ) + } } } .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) @@ -107,7 +120,7 @@ constructor( private data class State( override val value: Float, override val valueRange: ClosedFloatingPointRange<Float>, - override val icon: Icon, + override val icon: Icon.Loaded?, override val label: String, ) : SliderState { override val hapticFilter: SliderHapticFeedbackFilter @@ -116,7 +129,7 @@ constructor( override val isEnabled: Boolean get() = true - override val a11yStep: Float + override val step: Float get() = 1f override val disabledMessage: String? @@ -125,6 +138,9 @@ constructor( override val isMutable: Boolean get() = false + override val a11yContentDescription: String + get() = label + override val a11yClickDescription: String? get() = null diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 9d32285fecb3..9fe0ad42cdba 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.AudioManager import android.util.Log +import androidx.annotation.DrawableRes import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice @@ -28,6 +29,7 @@ import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.modes.shared.ModesUiIcons @@ -40,18 +42,21 @@ import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext /** Models a particular slider state. */ class AudioStreamSliderViewModel @@ -59,10 +64,11 @@ class AudioStreamSliderViewModel constructor( @Assisted private val audioStreamWrapper: FactoryAudioStreamWrapper, @Assisted private val coroutineScope: CoroutineScope, + @UiBackground private val uiBackgroundContext: CoroutineContext, private val context: Context, private val audioVolumeInteractor: AudioVolumeInteractor, private val zenModeInteractor: ZenModeInteractor, - private val audioSharingInteractor: AudioSharingInteractor, + audioSharingInteractor: AudioSharingInteractor, private val uiEventLogger: UiEventLogger, private val volumePanelLogger: VolumePanelLogger, private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, @@ -148,57 +154,69 @@ constructor( null } - private fun AudioStreamModel.toState( + private suspend fun AudioStreamModel.toState( isEnabled: Boolean, ringerMode: RingerMode, disabledMessage: String?, inAudioSharing: Boolean, primaryDevice: CachedBluetoothDevice?, - ): State { - val label = getLabel(inAudioSharing, primaryDevice) - val icon = getIcon(ringerMode, inAudioSharing) - return State( - value = volume.toFloat(), - valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), - hapticFilter = createHapticFilter(ringerMode), - icon = icon, - label = label, - disabledMessage = disabledMessage, - isEnabled = isEnabled, - a11yStep = volumeRange.step.toFloat(), - a11yClickDescription = - if (isAffectedByMute) { - context.getString( - if (isMuted) { - R.string.volume_panel_hint_unmute - } else { - R.string.volume_panel_hint_mute - }, - label, - ) - } else { - null - }, - a11yStateDescription = - if (isMuted) { - context.getString( - if (isAffectedByRingerMode) { - if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { - R.string.volume_panel_hint_vibrate + ): State = + withContext(uiBackgroundContext) { + val label = getLabel(inAudioSharing, primaryDevice) + State( + value = volume.toFloat(), + valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), + hapticFilter = createHapticFilter(ringerMode), + icon = getIcon(ringerMode, inAudioSharing), + label = label, + disabledMessage = disabledMessage, + isEnabled = isEnabled, + step = volumeRange.step.toFloat(), + a11yContentDescription = + if (isEnabled) { + label + } else { + disabledMessage?.let { + context.getString( + R.string.volume_slider_disabled_message_template, + label, + disabledMessage, + ) + } ?: label + }, + a11yClickDescription = + if (isAffectedByMute) { + context.getString( + if (isMuted) { + R.string.volume_panel_hint_unmute + } else { + R.string.volume_panel_hint_mute + }, + label, + ) + } else { + null + }, + a11yStateDescription = + if (isMuted) { + context.getString( + if (isAffectedByRingerMode) { + if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { + R.string.volume_panel_hint_vibrate + } else { + R.string.volume_panel_hint_muted + } } else { R.string.volume_panel_hint_muted } - } else { - R.string.volume_panel_hint_muted - } - ) - } else { - null - }, - audioStreamModel = this, - isMutable = isAffectedByMute, - ) - } + ) + } else { + null + }, + audioStreamModel = this@toState, + isMutable = isAffectedByMute, + ) + } private fun AudioStreamModel.createHapticFilter( ringerMode: RingerMode @@ -220,12 +238,14 @@ constructor( flowOf(context.getString(R.string.stream_notification_unavailable)) } else { if (zenModeInteractor.canBeBlockedByZenMode(audioStream)) { - zenModeInteractor.activeModesBlockingStream(audioStream).map { blockingZenModes - -> - blockingZenModes.mainMode?.name?.let { - context.getString(R.string.stream_unavailable_by_modes, it) - } ?: context.getString(R.string.stream_unavailable_by_unknown) - } + zenModeInteractor + .activeModesBlockingStream(audioStream) + .map { blockingZenModes -> + blockingZenModes.mainMode?.name?.let { + context.getString(R.string.stream_unavailable_by_modes, it) + } ?: context.getString(R.string.stream_unavailable_by_unknown) + } + .distinctUntilChanged() } else { flowOf(context.getString(R.string.stream_unavailable_by_unknown)) } @@ -256,8 +276,11 @@ constructor( ?: error("No label for the stream: $audioStream") } - private fun AudioStreamModel.getIcon(ringerMode: RingerMode, inAudioSharing: Boolean): Icon { - val iconRes = + private fun AudioStreamModel.getIcon( + ringerMode: RingerMode, + inAudioSharing: Boolean, + ): Icon.Loaded { + val iconResource: Int = if (isMuted) { if (isAffectedByRingerMode) { if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { @@ -272,14 +295,21 @@ constructor( inAudioSharing ) { R.drawable.ic_volume_media_bt_mute - } else R.drawable.ic_volume_off + } else { + R.drawable.ic_volume_off + } } } else { getIconByStream(audioStream, inAudioSharing) } - return Icon.Resource(iconRes, null) + return Icon.Loaded( + drawable = context.getDrawable(iconResource)!!, + contentDescription = null, + res = iconResource, + ) } + @DrawableRes private fun getIconByStream(audioStream: AudioStream, inAudioSharing: Boolean): Int = when (audioStream.value) { AudioManager.STREAM_MUSIC -> @@ -302,14 +332,15 @@ constructor( private data class State( override val value: Float, override val valueRange: ClosedFloatingPointRange<Float>, + override val step: Float, override val hapticFilter: SliderHapticFeedbackFilter, - override val icon: Icon, + override val icon: Icon.Loaded?, override val label: String, override val disabledMessage: String?, override val isEnabled: Boolean, - override val a11yStep: Float, override val a11yClickDescription: String?, override val a11yStateDescription: String?, + override val a11yContentDescription: String, override val isMutable: Boolean, val audioStreamModel: AudioStreamModel, ) : SliderState diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index a6c809186ca5..01810f9aafc3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -21,6 +21,7 @@ import android.media.session.MediaController.PlaybackInfo import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R @@ -30,30 +31,40 @@ import com.android.systemui.volume.panel.shared.VolumePanelLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext class CastVolumeSliderViewModel @AssistedInject constructor( @Assisted private val session: MediaDeviceSession, @Assisted private val coroutineScope: CoroutineScope, + @UiBackground private val uiBackgroundContext: CoroutineContext, private val context: Context, private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, private val volumePanelLogger: VolumePanelLogger, ) : SliderViewModel { + private val castLabel = context.getString(R.string.media_device_cast) + private val castIcon = + Icon.Loaded( + drawable = context.getDrawable(R.drawable.ic_cast)!!, + contentDescription = null, + res = R.drawable.ic_cast, + ) override val slider: StateFlow<SliderState> = mediaDeviceSessionInteractor .playbackInfo(session) .mapNotNull { volumePanelLogger.onVolumeUpdateReceived(session.sessionToken, it.currentVolume) - it.getCurrentState() + withContext(uiBackgroundContext) { it.getCurrentState() } } .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) @@ -83,20 +94,20 @@ constructor( return State( value = currentVolume.toFloat(), valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), - icon = Icon.Resource(R.drawable.ic_cast, null), - label = context.getString(R.string.media_device_cast), + icon = castIcon, + label = castLabel, isEnabled = true, - a11yStep = 1f, + step = 1f, ) } private data class State( override val value: Float, override val valueRange: ClosedFloatingPointRange<Float>, - override val icon: Icon, + override val icon: Icon.Loaded?, override val label: String, override val isEnabled: Boolean, - override val a11yStep: Float, + override val step: Float, ) : SliderState { override val hapticFilter: SliderHapticFeedbackFilter get() = SliderHapticFeedbackFilter() @@ -107,6 +118,9 @@ constructor( override val isMutable: Boolean get() = false + override val a11yContentDescription: String + get() = label + override val a11yClickDescription: String? get() = null diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt index 4bc237bd36f5..b1d183404a9f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt @@ -27,18 +27,17 @@ import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter sealed interface SliderState { val value: Float val valueRange: ClosedFloatingPointRange<Float> + val step: Float val hapticFilter: SliderHapticFeedbackFilter - val icon: Icon? + // Force preloaded icon + val icon: Icon.Loaded? val isEnabled: Boolean val label: String - /** - * A11y slider controls works by adjusting one step up or down. The default slider step isn't - * enough to trigger rounding to the correct value. - */ - val a11yStep: Float + val a11yClickDescription: String? val a11yStateDescription: String? + val a11yContentDescription: String val disabledMessage: String? val isMutable: Boolean @@ -46,12 +45,13 @@ sealed interface SliderState { override val value: Float = 0f override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f override val hapticFilter = SliderHapticFeedbackFilter() - override val icon: Icon? = null + override val icon: Icon.Loaded? = null override val label: String = "" override val disabledMessage: String? = null - override val a11yStep: Float = 0f + override val step: Float = 0f override val a11yClickDescription: String? = null override val a11yStateDescription: String? = null + override val a11yContentDescription: String = label override val isEnabled: Boolean = true override val isMutable: Boolean = false } diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt index f6582a005035..502b311f7b40 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.clearAndSetSemantics @@ -52,7 +51,6 @@ import androidx.compose.ui.semantics.stateDescription import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.res.R import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider import kotlin.math.round import kotlinx.coroutines.Job @@ -108,7 +106,8 @@ fun Slider( } } val semantics = - accessibilityParams.createSemantics( + createSemantics( + accessibilityParams, animatable.targetValue, valueRange, valueChange, @@ -167,24 +166,18 @@ private fun snapValue( return Math.round(coercedValue / stepDistance) * stepDistance } -@Composable -private fun AccessibilityParams.createSemantics( +private fun createSemantics( + params: AccessibilityParams, value: Float, valueRange: ClosedFloatingPointRange<Float>, onValueChanged: (Float) -> Unit, isEnabled: Boolean, stepDistance: Float, ): SemanticsPropertyReceiver.() -> Unit { - val semanticsContentDescription = - disabledMessage - ?.takeIf { !isEnabled } - ?.let { message -> - stringResource(R.string.volume_slider_disabled_message_template, label, message) - } ?: label return { - contentDescription = semanticsContentDescription + contentDescription = params.contentDescription if (isEnabled) { - currentStateDescription?.let { stateDescription = it } + params.stateDescription?.let { stateDescription = it } progressBarRangeInfo = ProgressBarRangeInfo(value, valueRange) } else { disabled() @@ -253,9 +246,8 @@ private fun Haptics.createViewModel( } data class AccessibilityParams( - val label: String, - val currentStateDescription: String? = null, - val disabledMessage: String? = null, + val contentDescription: String, + val stateDescription: String? = null, ) sealed interface Haptics { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt index 09f9f1c6362e..44d7a22b6258 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.volume.domain.interactor.audioVolumeInteractor @@ -25,6 +26,7 @@ val Kosmos.volumeDialogSliderIconProvider by Kosmos.Fixture { VolumeDialogSliderIconProvider( context = applicationContext, + uiBackgroundContext = backgroundCoroutineContext, audioVolumeInteractor = audioVolumeInteractor, zenModeInteractor = zenModeInteractor, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt index 8c8d0240f572..6e43d79295ff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt @@ -16,9 +16,11 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel +import android.content.applicationContext import com.android.internal.logging.uiEventLogger import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.volume.domain.interactor.audioSharingInteractor import com.android.systemui.volume.shared.volumePanelLogger import kotlinx.coroutines.CoroutineScope @@ -28,7 +30,9 @@ val Kosmos.audioSharingStreamSliderViewModelFactory by object : AudioSharingStreamSliderViewModel.Factory { override fun create(coroutineScope: CoroutineScope): AudioSharingStreamSliderViewModel { return AudioSharingStreamSliderViewModel( + applicationContext, coroutineScope, + backgroundCoroutineContext, audioSharingInteractor, uiEventLogger, sliderHapticsViewModelFactory, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt index 88c716e0ab10..47016e535ea4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt @@ -20,6 +20,7 @@ import android.content.applicationContext import com.android.internal.logging.uiEventLogger import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.volume.domain.interactor.audioSharingInteractor import com.android.systemui.volume.domain.interactor.audioVolumeInteractor @@ -37,6 +38,7 @@ val Kosmos.audioStreamSliderViewModelFactory by return AudioStreamSliderViewModel( audioStream, coroutineScope, + backgroundCoroutineContext, applicationContext, audioVolumeInteractor, zenModeInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt index 6875619d45fc..ed51e054e50c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.applicationContext import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession import com.android.systemui.volume.shared.volumePanelLogger @@ -34,6 +35,7 @@ val Kosmos.castVolumeSliderViewModelFactory by return CastVolumeSliderViewModel( session, coroutineScope, + backgroundCoroutineContext, applicationContext, mediaDeviceSessionInteractor, sliderHapticsViewModelFactory, |