[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
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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<RoomDetailsEd
|
||||
aRoomDetailsEditState(),
|
||||
aRoomDetailsEditState(roomTopic = ""),
|
||||
aRoomDetailsEditState(roomRawName = ""),
|
||||
aRoomDetailsEditState(roomAvatarUrl = Uri.parse("example://uri")),
|
||||
aRoomDetailsEditState(roomAvatarUrl = "example://uri".toUri()),
|
||||
aRoomDetailsEditState(canChangeName = true, canChangeTopic = false, canChangeAvatar = true, saveButtonEnabled = false),
|
||||
aRoomDetailsEditState(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
|
||||
aRoomDetailsEditState(saveAction = AsyncAction.Loading),
|
||||
|
||||
@@ -9,12 +9,12 @@ package io.element.android.libraries.androidutils.browser
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Browser
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import androidx.core.net.toUri
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import java.util.Locale
|
||||
|
||||
@@ -58,7 +58,7 @@ fun Activity.openUrlInChromeCustomTab(
|
||||
putString("Accept-Language", Locale.getDefault().toLanguageTag())
|
||||
})
|
||||
}
|
||||
.launchUrl(this, Uri.parse(url))
|
||||
.launchUrl(this, url.toUri())
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
openUrlInExternalApp(url)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import androidx.core.net.toUri
|
||||
import io.element.android.libraries.androidutils.R
|
||||
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
@@ -121,7 +122,7 @@ fun Context.startInstallFromSourceIntent(
|
||||
noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.setData("package:$packageName".toUri())
|
||||
try {
|
||||
activityResultLauncher.launch(intent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
@@ -165,7 +166,7 @@ fun Context.openUrlInExternalApp(
|
||||
url: String,
|
||||
errorMessage: String = getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
if (this !is Activity) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
package io.element.android.libraries.androidutils.uri
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
|
||||
const val IGNORED_SCHEMA = "ignored"
|
||||
|
||||
fun createIgnoredUri(path: String): Uri = Uri.parse("$IGNORED_SCHEMA://$path")
|
||||
fun createIgnoredUri(path: String): Uri = "$IGNORED_SCHEMA://$path".toUri()
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -59,6 +60,7 @@ fun Avatar(
|
||||
avatarData = avatarData,
|
||||
forcedAvatarSize = forcedAvatarSize,
|
||||
modifier = commonModifier,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
} else {
|
||||
ImageAvatar(
|
||||
@@ -103,11 +105,13 @@ private fun ImageAvatar(
|
||||
InitialsAvatar(
|
||||
avatarData = avatarData,
|
||||
forcedAvatarSize = forcedAvatarSize,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
else -> 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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Uri?> {
|
||||
override val values: Sequence<Uri?>
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user