Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 615c636c authored by Anton Potapov's avatar Anton Potapov
Browse files

Add custom a11y actions and state description for VolumeSlider

Flag: aconfig new_volume_panel NEXTFOOD
Test: manual on phone. Focus muted sliders with voiceover enabled.
Fixes: 339162398
Fixes: 331896374
Change-Id: I840553df905324d590dc1243dc7df0d7e09e5a59
parent c6a68fea
Loading
Loading
Loading
Loading
+27 −7
Original line number Diff line number Diff line
@@ -34,12 +34,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.clearAndSetSemantics
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.setProgress
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSlider
import com.android.compose.PlatformSliderColors
@@ -60,14 +63,31 @@ fun VolumeSlider(
    PlatformSlider(
        modifier =
            modifier.clearAndSetSemantics {
                if (!state.isEnabled) disabled()
                if (state.isEnabled) {
                    contentDescription = state.label
                    state.a11yClickDescription?.let {
                        customActions =
                            listOf(
                                CustomAccessibilityAction(
                                    it,
                                ) {
                                    onIconTapped()
                                    true
                                }
                            )
                    }

                    state.a11yStateDescription?.let { stateDescription = it }
                        ?: run {
                            // provide a not animated value to the a11y because it fails to announce
                            // the settled value when it changes rapidly.
                            progressBarRangeInfo =
                                ProgressBarRangeInfo(state.value, state.valueRange)
                        }
                } else {
                    disabled()
                    contentDescription =
                        state.disabledMessage?.let { "${state.label}, $it" } ?: state.label

                // provide a not animated value to the a11y because it fails to announce the
                // settled value when it changes rapidly.
                if (state.isEnabled) {
                    progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
                }
                setProgress { targetValue ->
                    val targetDirection =
+8 −2
Original line number Diff line number Diff line
@@ -1634,9 +1634,15 @@
    <string name="volume_panel_collapsed_sliders">Volume sliders collapsed</string>

    <!-- Hint for accessibility. A stream name is a parameter. For example: double tap to mute media [CHAR_LIMIT=NONE] -->
    <string name="volume_panel_hint_mute">mute %s</string>
    <string name="volume_panel_hint_mute">Mute %s</string>
    <!-- Hint for accessibility. A stream name is a parameter. For example: double tap to unmute media [CHAR_LIMIT=NONE] -->
    <string name="volume_panel_hint_unmute">unmute %s</string>
    <string name="volume_panel_hint_unmute">Unmute %s</string>

    <!-- Hint for accessibility. This is announced when the stream is muted [CHAR_LIMIT=NONE] -->
    <string name="volume_panel_hint_muted">muted</string>

    <!-- Hint for accessibility. This is announced when ring mode is set to Vibrate. [CHAR_LIMIT=NONE] -->
    <string name="volume_panel_hint_vibrate">vibrate</string>

    <!-- Title with application label for media output settings when there is media playing. [CHAR LIMIT=20] -->
    <string name="media_output_label_title">Playing <xliff:g id="label" example="Music Player">%s</xliff:g> on</string>
+42 −21
Original line number Diff line number Diff line
@@ -49,6 +49,11 @@ constructor(
    private val uiEventLogger: UiEventLogger,
) : SliderViewModel {

    private val streamsAffectedByRing =
        setOf(
            AudioManager.STREAM_RING,
            AudioManager.STREAM_NOTIFICATION,
        )
    private val audioStream = audioStreamWrapper.audioStream
    private val iconsByStream =
        mapOf(
@@ -125,15 +130,42 @@ constructor(
        isEnabled: Boolean,
        ringerMode: RingerMode,
    ): State {
        val label =
            labelsByStream[audioStream]?.let(context::getString)
                ?: error("No label for the stream: $audioStream")
        return State(
            value = volume.toFloat(),
            valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
            icon = getIcon(ringerMode),
            label = labelsByStream[audioStream]?.let(context::getString)
                    ?: error("No label for the stream: $audioStream"),
            label = label,
            disabledMessage = disabledTextByStream[audioStream]?.let(context::getString),
            isEnabled = isEnabled,
            a11yStep = volumeRange.step,
            a11yClickDescription =
                context.getString(
                    if (isMuted) {
                        R.string.volume_panel_hint_unmute
                    } else {
                        R.string.volume_panel_hint_mute
                    },
                    label,
                ),
            a11yStateDescription =
                if (volume == volumeRange.first) {
                    context.getString(
                        if (audioStream.value in streamsAffectedByRing) {
                            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 {
                    null
                },
            audioStreamModel = this,
            isMutable = audioVolumeInteractor.isAffectedByMute(audioStream),
        )
@@ -143,28 +175,15 @@ constructor(
        val isMutedOrNoVolume = isMuted || volume == minVolume
        val iconRes =
            if (isMutedOrNoVolume) {
                when (audioStream.value) {
                    AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_off
                    AudioManager.STREAM_BLUETOOTH_SCO -> R.drawable.ic_volume_off
                    AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_volume_off
                    AudioManager.STREAM_RING ->
                if (audioStream.value in streamsAffectedByRing) {
                    if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
                        R.drawable.ic_volume_ringer_vibrate
                    } else {
                        R.drawable.ic_volume_off
                    }
                    AudioManager.STREAM_NOTIFICATION ->
                        if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
                            R.drawable.ic_volume_ringer_vibrate
                } else {
                    R.drawable.ic_volume_off
                }
                    AudioManager.STREAM_ALARM -> R.drawable.ic_volume_off
                    else -> {
                        Log.wtf(TAG, "No icon for the stream: $audioStream")
                        R.drawable.ic_volume_off
                    }
                }
            } else {
                iconsByStream[audioStream]
                    ?: run {
@@ -186,6 +205,8 @@ constructor(
        override val disabledMessage: String?,
        override val isEnabled: Boolean,
        override val a11yStep: Int,
        override val a11yClickDescription: String?,
        override val a11yStateDescription: String?,
        override val isMutable: Boolean,
        val audioStreamModel: AudioStreamModel,
    ) : SliderState
+7 −1
Original line number Diff line number Diff line
@@ -68,7 +68,7 @@ constructor(
            icon = Icon.Resource(R.drawable.ic_cast, null),
            label = context.getString(R.string.media_device_cast),
            isEnabled = true,
            a11yStep = 1
            a11yStep = 1,
        )
    }

@@ -85,6 +85,12 @@ constructor(

        override val isMutable: Boolean
            get() = false

        override val a11yClickDescription: String?
            get() = null

        override val a11yStateDescription: String?
            get() = null
    }

    @AssistedFactory
+4 −0
Original line number Diff line number Diff line
@@ -34,6 +34,8 @@ sealed interface SliderState {
     * enough to trigger rounding to the correct value.
     */
    val a11yStep: Int
    val a11yClickDescription: String?
    val a11yStateDescription: String?
    val disabledMessage: String?
    val isMutable: Boolean

@@ -44,6 +46,8 @@ sealed interface SliderState {
        override val label: String = ""
        override val disabledMessage: String? = null
        override val a11yStep: Int = 0
        override val a11yClickDescription: String? = null
        override val a11yStateDescription: String? = null
        override val isEnabled: Boolean = true
        override val isMutable: Boolean = false
    }