diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts
index f2dbf1aacf..b723578829 100644
--- a/libraries/mediaviewer/impl/build.gradle.kts
+++ b/libraries/mediaviewer/impl/build.gradle.kts
@@ -50,6 +50,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.uiUtils)
implementation(projects.libraries.voiceplayer.api)
implementation(projects.services.toolbox.api)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt
index b06b97f491..1f2b0b93d2 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt
@@ -28,6 +28,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -100,36 +103,54 @@ fun MediaPlayerControllerView(
contentColor = ElementTheme.colors.iconOnSolidPrimary,
)
}
+ val a11yPause = stringResource(CommonStrings.a11y_pause)
+ val a11yPlay = stringResource(CommonStrings.a11y_play)
IconButton(
modifier = Modifier
- .size(36.dp),
+ .size(36.dp)
+ .semantics {
+ stateDescription = if (state.isPlaying) a11yPause else a11yPlay
+ },
onClick = onTogglePlay,
colors = colors,
) {
if (state.isPlaying) {
Icon(
imageVector = CompoundIcons.PauseSolid(),
- contentDescription = stringResource(CommonStrings.a11y_pause)
+ contentDescription = null,
)
} else {
Icon(
imageVector = CompoundIcons.PlaySolid(),
- contentDescription = stringResource(CommonStrings.a11y_play)
+ contentDescription = null,
)
}
}
+ val position = state.displayProgressInMillis.toHumanReadableDuration()
+ val a11yPosition = stringResource(CommonStrings.a11y_position, position)
Text(
modifier = Modifier
.widthIn(min = 48.dp)
- .padding(horizontal = 8.dp),
- text = state.displayProgressInMillis.toHumanReadableDuration(),
+ .padding(horizontal = 8.dp)
+ .semantics {
+ contentDescription = a11yPosition
+ },
+ text = position,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontBodyXsMedium,
)
var lastSelectedValue by remember { mutableFloatStateOf(-1f) }
Slider(
- modifier = Modifier.weight(1f),
+ modifier = Modifier
+ .weight(1f)
+ .semantics {
+ // Speak out a progress percent instead of milliseconds
+ stateDescription = buildString {
+ append((state.progressAsFloat * 100).toInt())
+ append("%")
+ }
+ },
valueRange = 0f..state.durationInMillis.toFloat(),
value = lastSelectedValue.takeIf { it >= 0 }
?: state.seekingToMillis?.toFloat()
@@ -146,30 +167,40 @@ fun MediaPlayerControllerView(
val formattedDuration = remember(state.durationInMillis) {
state.durationInMillis.toHumanReadableDuration()
}
+ val a11yDuration = stringResource(CommonStrings.a11y_duration, formattedDuration)
Text(
modifier = Modifier
.widthIn(min = 48.dp)
- .padding(horizontal = 8.dp),
+ .padding(horizontal = 8.dp)
+ .semantics {
+ contentDescription = a11yDuration
+ },
text = formattedDuration,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontBodyXsMedium,
)
if (state.canMute) {
+ val a11yUnmute = stringResource(CommonStrings.common_unmute)
+ val a11yMute = stringResource(CommonStrings.common_mute)
IconButton(
onClick = onToggleMute,
+ modifier = Modifier
+ .semantics {
+ stateDescription = if (state.isMuted) a11yUnmute else a11yMute
+ },
) {
if (state.isMuted) {
Icon(
imageVector = CompoundIcons.VolumeOffSolid(),
tint = ElementTheme.colors.iconPrimary,
- contentDescription = stringResource(CommonStrings.common_unmute)
+ contentDescription = null,
)
} else {
Icon(
imageVector = CompoundIcons.VolumeOnSolid(),
tint = ElementTheme.colors.iconPrimary,
- contentDescription = stringResource(CommonStrings.common_mute)
+ contentDescription = null,
)
}
}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt
index 9d5e290857..d89762b72e 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt
@@ -57,6 +57,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPla
import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying
import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
+import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import kotlinx.coroutines.delay
import me.saket.telephoto.zoomable.zoomable
import timber.log.Timber
@@ -162,12 +163,20 @@ private fun ExoPlayerMediaVideoView(
var autoHideController by remember { mutableIntStateOf(0) }
- LaunchedEffect(autoHideController) {
- delay(5.seconds)
- if (exoPlayer.isPlaying) {
+ val isTalkbackActive = isTalkbackActive()
+ LaunchedEffect(autoHideController, isTalkbackActive) {
+ if (isTalkbackActive) {
+ // Ensure that the controller is always visible when talkback is active
mediaPlayerControllerState = mediaPlayerControllerState.copy(
- isVisible = false,
+ isVisible = true,
)
+ } else {
+ delay(5.seconds)
+ if (exoPlayer.isPlaying) {
+ mediaPlayerControllerState = mediaPlayerControllerState.copy(
+ isVisible = false,
+ )
+ }
}
}
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index ee0f51000a..fd3679a140 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -9,6 +9,7 @@
- "%1$d digit entered"
- "%1$d digits entered"
+ "Duration: %1$s"
"Edit avatar"
"The full address will be %1$s"
"Encryption details"
@@ -33,6 +34,7 @@
"Playback speed"
"Poll"
"Ended poll"
+ "Position: %1$s"
"QR Code"
"React with %1$s"
"React with other emojis"