From e2e58749f50d4b001e1b1a6d270780fd265c2a70 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 22 Apr 2025 08:50:50 +0200 Subject: [PATCH] [a11y] Make more items focusable (#4605) * Fix settings entry point not available when there is no avatar on the account. Fixes #4599. * Use Ktx extension `String.toUri()` * Allow screen reader to focus on the user avatar to allow editing it. * Fix import order --- .../call/impl/utils/CallIntentDataParser.kt | 5 +++-- .../android/features/login/impl/util/Util.kt | 4 ++-- .../web/WebClientUrlForAuthenticationRetriever.kt | 4 ++-- .../user/editprofile/EditUserProfilePresenter.kt | 2 +- .../impl/edit/RoomDetailsEditStateProvider.kt | 3 ++- .../androidutils/browser/ChromeCustomTab.kt | 4 ++-- .../libraries/androidutils/system/SystemUtils.kt | 5 +++-- .../libraries/androidutils/uri/UriExtensions.kt | 3 ++- .../designsystem/components/avatar/Avatar.kt | 11 ++++++++++- .../impl/FullScreenIntentPermissionsPresenter.kt | 4 ++-- .../impl/permalink/DefaultMatrixToConverter.kt | 3 ++- .../matrix/impl/permalink/DefaultPermalinkParser.kt | 4 ++-- .../matrix/ui/components/EditableAvatarView.kt | 13 +++++++++++-- .../oidc/impl/customtab/CustomTabHandler.kt | 4 ++-- .../notifications/model/NotifiableMessageEvent.kt | 3 ++- 15 files changed, 48 insertions(+), 24 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt index 827427e0cf..c81b87746e 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt @@ -8,6 +8,7 @@ package io.element.android.features.call.impl.utils import android.net.Uri +import androidx.core.net.toUri import javax.inject.Inject class CallIntentDataParser @Inject constructor() { @@ -17,7 +18,7 @@ class CallIntentDataParser @Inject constructor() { ) fun parse(data: String?): String? { - val parsedUrl = data?.let { Uri.parse(data) } ?: return null + val parsedUrl = data?.toUri() ?: return null val scheme = parsedUrl.scheme return when { scheme in validHttpSchemes -> parsedUrl @@ -37,7 +38,7 @@ class CallIntentDataParser @Inject constructor() { private fun Uri.getUrlParameter(): Uri? { return getQueryParameter("url") ?.let { urlParameter -> - Uri.parse(urlParameter).takeIf { uri -> + urlParameter.toUri().takeIf { uri -> uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank() } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt index 6c10faa05b..36ee64c600 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt @@ -9,11 +9,11 @@ package io.element.android.features.login.impl.util import android.content.Context import android.content.Intent -import android.net.Uri +import androidx.core.net.toUri import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.data.tryOrNull fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(AuthenticationConfig.SLIDING_SYNC_READ_MORE_URL)) + val intent = Intent(Intent.ACTION_VIEW, AuthenticationConfig.SLIDING_SYNC_READ_MORE_URL.toUri()) tryOrNull { context.startActivity(intent) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt index a5d9ec754a..3793f3a53b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt @@ -7,7 +7,7 @@ package io.element.android.features.login.impl.web -import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.resolver.network.WellknownAPI @@ -43,7 +43,7 @@ class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor( } val registrationHelperUrl = result.registrationHelperUrl return if (registrationHelperUrl != null) { - Uri.parse(registrationHelperUrl) + registrationHelperUrl.toUri() .buildUpon() .appendQueryParameter("hs_url", homeServerUrl) .build() diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index 9d57829360..1464ecf584 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -58,7 +58,7 @@ class EditUserProfilePresenter @AssistedInject constructor( @Composable override fun present(): EditUserProfileState { val cameraPermissionState = cameraPermissionPresenter.present() - var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) } + var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.toUri()) } var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( onResult = { uri -> diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt index 46304e7aaf..0b979be743 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl.edit import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.core.net.toUri import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -22,7 +23,7 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider InitialsAvatar( avatarData = avatarData, forcedAvatarSize = forcedAvatarSize, + contentDescription = contentDescription, ) } } @@ -118,6 +122,7 @@ private fun ImageAvatar( private fun InitialsAvatar( avatarData: AvatarData, forcedAvatarSize: Dp?, + contentDescription: String?, modifier: Modifier = Modifier, ) { val avatarColors = AvatarColorsProvider.provide(avatarData.id) @@ -130,7 +135,11 @@ private fun InitialsAvatar( val lineHeight = originalFont.lineHeight * ratio Text( modifier = Modifier - .clearAndSetSemantics {} + .clearAndSetSemantics { + contentDescription?.let { + this.contentDescription = it + } + } .align(Alignment.Center), text = avatarData.initial, style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt index 992c345e5c..3e427b6fa6 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.fullscreenintent.impl import android.content.ActivityNotFoundException import android.content.Intent -import android.net.Uri import android.os.Build import android.provider.Settings import androidx.compose.runtime.Composable @@ -17,6 +16,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.core.app.NotificationManagerCompat +import androidx.core.net.toUri import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import io.element.android.libraries.architecture.Presenter @@ -77,7 +77,7 @@ class FullScreenIntentPermissionsPresenter @Inject constructor( try { val intent = Intent( Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, - Uri.parse("package:${buildMeta.applicationId}") + "package:${buildMeta.applicationId}".toUri() ) externalIntentLauncher.launch(intent) } catch (e: ActivityNotFoundException) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt index c578ca5cc8..9e881f18aa 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.permalink import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.MatrixConfiguration import io.element.android.libraries.core.extensions.replacePrefix @@ -43,7 +44,7 @@ class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter { // Web or client url SUPPORTED_PATHS.any { it in uriString } -> { val path = SUPPORTED_PATHS.first { it in uriString } - Uri.parse(baseUrl + uriString.substringAfter(path)) + (baseUrl + uriString.substringAfter(path)).toUri() } // URL is not supported else -> null diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt index d27a9a79df..80f59b9469 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.impl.permalink -import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.EventId @@ -38,7 +38,7 @@ class DefaultPermalinkParser @Inject constructor( * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md */ override fun parse(uriString: String): PermalinkData { - val uri = Uri.parse(uriString) + val uri = uriString.toUri() // the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the // mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid // so convert URI to matrix.to to simplify parsing process diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt index 1a0cd53f6d..eb46482c5c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -23,9 +23,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -36,6 +40,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun EditableAvatarView( @@ -50,6 +55,7 @@ fun EditableAvatarView( modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { + val a11yAvatar = stringResource(CommonStrings.a11y_avatar) Box( modifier = Modifier .size(avatarSize.dp) @@ -59,6 +65,9 @@ fun EditableAvatarView( indication = ripple(bounded = false), ) .testTag(TestTags.editAvatar) + .clearAndSetSemantics { + contentDescription = a11yAvatar + }, ) { when (avatarUrl?.scheme) { null, "mxc" -> { @@ -112,7 +121,7 @@ open class EditableAvatarViewUriProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( null, - Uri.parse("mxc://matrix.org/123456"), - Uri.parse("https://example.com/avatar.jpg"), + "mxc://matrix.org/123456".toUri(), + "https://example.com/avatar.jpg".toUri(), ) } diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt index 42d1391e9f..64c21596c1 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt @@ -10,10 +10,10 @@ package io.element.android.libraries.oidc.impl.customtab import android.app.Activity import android.content.ComponentName import android.content.Context -import android.net.Uri import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession +import androidx.core.net.toUri import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -55,7 +55,7 @@ class CustomTabHandler @Inject constructor( customTabsSession = customTabsClient?.newSession(null) } - customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + customTabsSession?.mayLaunchUrl(url.toUri(), null, null) } fun disposeCustomTab() { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt index 21feeb1cd3..ba7cd2f78b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.push.impl.notifications.model import android.net.Uri +import androidx.core.net.toUri import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -52,7 +53,7 @@ data class NotifiableMessageEvent( // Example of value: // content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ val imageUri: Uri? - get() = imageUriString?.let { Uri.parse(it) } + get() = imageUriString?.toUri() } /**