Merge pull request #8426 from vector-im/feature/bma/a11yFixes

A11y fixes
This commit is contained in:
Benoit Marty 2023-05-15 16:28:35 +02:00 committed by GitHub
commit 7c3ecec92a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 242 additions and 55 deletions

1
changelog.d/8426.misc Normal file
View file

@ -0,0 +1 @@
Improve keyboard navigation and accessibility when using a screen reader.

View file

@ -1470,6 +1470,9 @@
<string name="reason_colon">Reason: %1$s</string> <string name="reason_colon">Reason: %1$s</string>
<string name="avatar">Avatar</string> <string name="avatar">Avatar</string>
<string name="avatar_of_space">Avatar of space %1$s</string>
<string name="avatar_of_room">Avatar of room %1$s</string>
<string name="avatar_of_user">Profile picture of user %1$s</string>
<!-- Consent modal --> <!-- Consent modal -->
<string name="dialog_user_consent_content">To continue using the %1$s homeserver you must review and agree to the terms and conditions.</string> <string name="dialog_user_consent_content">To continue using the %1$s homeserver you must review and agree to the terms and conditions.</string>

View file

@ -4,6 +4,9 @@
<style name="Widget.Vector.AppBarLayout" parent="Widget.MaterialComponents.AppBarLayout.Primary"> <style name="Widget.Vector.AppBarLayout" parent="Widget.MaterialComponents.AppBarLayout.Primary">
<item name="android:background">?vctr_toolbar_background</item> <item name="android:background">?vctr_toolbar_background</item>
<item name="elevation">4dp</item> <item name="elevation">4dp</item>
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style> </style>
</resources> </resources>

View file

@ -12,6 +12,9 @@
<item name="subtitleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarSubTitle</item> <item name="subtitleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarSubTitle</item>
<item name="navigationIconTint">?vctr_content_secondary</item> <item name="navigationIconTint">?vctr_content_secondary</item>
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style> </style>
<!-- Default toolbar style --> <!-- Default toolbar style -->
@ -39,14 +42,24 @@
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
</style> </style>
<!-- Material 3 --> <!-- CollapsingToolbar -->
<style name="Widget.Vector.Material3.Toolbar" parent="Widget.Material3.Toolbar" /> <style name="Widget.Vector.CollapsingToolbar" parent="Widget.Material3.CollapsingToolbar">
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>
<style name="Widget.Vector.Material3.CollapsingToolbar.Medium" parent="Widget.Material3.CollapsingToolbar.Medium"> <style name="Widget.Vector.CollapsingToolbar.Medium" parent="Widget.Material3.CollapsingToolbar.Medium">
<item name="expandedTitleTextAppearance">@style/TextAppearance.Vector.Title.Medium</item> <item name="expandedTitleTextAppearance">@style/TextAppearance.Vector.Title.Medium</item>
<item name="expandedTitleMarginBottom">20dp</item> <item name="expandedTitleMarginBottom">20dp</item>
<item name="collapsedTitleTextAppearance">@style/TextAppearance.Vector.Headline.Bold</item> <item name="collapsedTitleTextAppearance">@style/TextAppearance.Vector.Headline.Bold</item>
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>
<style name="Widget.Vector.CollapsingToolbar.Large" parent="Widget.Material3.CollapsingToolbar.Large">
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style> </style>
</resources> </resources>

View file

@ -83,6 +83,9 @@
<item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item> <item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
<item name="materialButtonStyle">@style/Widget.Vector.Button</item> <item name="materialButtonStyle">@style/Widget.Vector.Button</item>
<item name="toolbarStyle">@style/Widget.Vector.Toolbar</item> <item name="toolbarStyle">@style/Widget.Vector.Toolbar</item>
<item name="collapsingToolbarLayoutStyle">@style/Widget.Vector.CollapsingToolbar</item>
<item name="collapsingToolbarLayoutMediumStyle">@style/Widget.Vector.CollapsingToolbar.Medium</item>
<item name="collapsingToolbarLayoutLargeStyle">@style/Widget.Vector.CollapsingToolbar.Large</item>
<item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item> <item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
<item name="searchViewStyle">@style/Widget.Vector.SearchView</item> <item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
<item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item> <item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>

View file

@ -83,6 +83,9 @@
<item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item> <item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
<item name="materialButtonStyle">@style/Widget.Vector.Button</item> <item name="materialButtonStyle">@style/Widget.Vector.Button</item>
<item name="toolbarStyle">@style/Widget.Vector.Toolbar</item> <item name="toolbarStyle">@style/Widget.Vector.Toolbar</item>
<item name="collapsingToolbarLayoutStyle">@style/Widget.Vector.CollapsingToolbar</item>
<item name="collapsingToolbarLayoutMediumStyle">@style/Widget.Vector.CollapsingToolbar.Medium</item>
<item name="collapsingToolbarLayoutLargeStyle">@style/Widget.Vector.CollapsingToolbar.Large</item>
<item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item> <item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
<item name="searchViewStyle">@style/Widget.Vector.SearchView</item> <item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
<item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item> <item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>

View file

@ -20,6 +20,8 @@ import android.graphics.drawable.Drawable
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
@ -28,6 +30,7 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.transition.ChangeBounds import androidx.transition.ChangeBounds
import androidx.transition.Fade import androidx.transition.Fade
@ -97,6 +100,14 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) {
setBackgroundResource(attribute.resourceId) setBackgroundResource(attribute.resourceId)
} }
/**
* Inspired from https://stackoverflow.com/a/64597532/1472514. Safer to call the 2 available API.
*/
fun View.giveAccessibilityFocus() {
ViewCompat.performAccessibilityAction(this, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
}
fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) { fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) {
val transition = TransitionSet().apply { val transition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_SEQUENTIAL ordering = TransitionSet.ORDERING_SEQUENTIAL

View file

@ -45,6 +45,7 @@ import im.vector.app.R
import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.di.ActivityEntryPoint
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.ToolbarConfig import im.vector.app.core.utils.ToolbarConfig
@ -318,4 +319,19 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
/* ==========================================================================================
* Accessibility - a11y
* ========================================================================================== */
private var hasBeenAccessibilityFocused = false
/**
* Ensure the View get the accessibility focus. This method has effect only once per fragment instance.
*/
protected fun View.giveAccessibilityFocusOnce() {
if (hasBeenAccessibilityFocused) return
hasBeenAccessibilityFocused = true
giveAccessibilityFocus()
}
} }

View file

@ -60,5 +60,6 @@ class BootstrapConclusionFragment :
.toSpannable() .toSpannable()
.colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
.colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
views.bootstrapConclusionText.giveAccessibilityFocusOnce()
} }
} }

View file

@ -52,6 +52,7 @@ class BootstrapConfirmPassphraseFragment :
views.ssssPassphraseSecurityProgress.isGone = true views.ssssPassphraseSecurityProgress.isGone = true
views.bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_again_notice) views.bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_again_notice)
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
views.ssssPassphraseEnterEdittext.hint = getString(R.string.set_a_security_phrase_hint) views.ssssPassphraseEnterEdittext.hint = getString(R.string.set_a_security_phrase_hint)
withState(sharedViewModel) { withState(sharedViewModel) {

View file

@ -118,5 +118,6 @@ class BootstrapEnterPassphraseFragment :
} }
} }
} }
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
} }
} }

View file

@ -141,6 +141,7 @@ class BootstrapMigrateBackupFragment :
views.bootstrapMigrateUseFile.isVisible = false views.bootstrapMigrateUseFile.isVisible = false
} }
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
} }
private val importFileStartForActivityResult = registerStartForActivityResult { activityResult -> private val importFileStartForActivityResult = registerStartForActivityResult { activityResult ->

View file

@ -78,5 +78,6 @@ class BootstrapReAuthFragment :
views.bootstrapCancelButton.isVisible = true views.bootstrapCancelButton.isVisible = true
views.bootstrapRetryButton.isVisible = true views.bootstrapRetryButton.isVisible = true
} }
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
} }
} }

View file

@ -117,5 +117,6 @@ class BootstrapSaveRecoveryKeyFragment :
views.recoveryContinue.isVisible = step.isSaved views.recoveryContinue.isVisible = step.isSaved
views.bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() views.bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey()
views.bootstrapSaveText.giveAccessibilityFocusOnce()
} }
} }

View file

@ -68,6 +68,7 @@ class BootstrapSetupRecoveryKeyFragment :
// Choose between create a passphrase or use a recovery key // Choose between create a passphrase or use a recovery key
renderBackupMethodActions(firstFormStep.methods) renderBackupMethodActions(firstFormStep.methods)
} }
views.bootstrapSetupSecureText.giveAccessibilityFocusOnce()
} }
private fun renderStateWithExistingKeyBackup() = with(views) { private fun renderStateWithExistingKeyBackup() = with(views) {

View file

@ -52,5 +52,6 @@ class BootstrapWaitingFragment :
views.bootstrapDescriptionText.isVisible = false views.bootstrapDescriptionText.isVisible = false
} }
} }
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
} }
} }

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.crypto.verification
import android.view.LayoutInflater
import android.view.ViewGroup
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentProgressBinding
@AndroidEntryPoint
class QuadSLoadingFragment :
VectorBaseFragment<FragmentProgressBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentProgressBinding {
return FragmentProgressBinding.inflate(inflater, container, false)
}
}

View file

@ -21,12 +21,18 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
@ -36,15 +42,26 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.qrcode.QrCodeScannerActivity import im.vector.app.features.qrcode.QrCodeScannerActivity
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(), class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
SelfVerificationController.InteractionListener { SelfVerificationController.InteractionListener {
@Inject lateinit var controller: SelfVerificationController @Inject lateinit var controller: SelfVerificationController
private var requestAccessibilityFocus: Boolean = false
private val modelBuildListener: OnModelBuildFinishedListener = OnModelBuildFinishedListener {
if (requestAccessibilityFocus) {
// Do not use giveAccessibilityFocusOnce() here.
views.bottomSheetVerificationRecyclerView.layoutManager?.findViewByPosition(0)?.giveAccessibilityFocus()
requestAccessibilityFocus = false
// Note: it does not work when the recycler view is displayed for the first time, because findViewByPosition(0) is null
}
}
private val viewModel by parentFragmentViewModel(SelfVerificationViewModel::class) private val viewModel by parentFragmentViewModel(SelfVerificationViewModel::class)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding {
@ -58,17 +75,22 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
override fun onDestroyView() { override fun onDestroyView() {
views.bottomSheetVerificationRecyclerView.cleanup() views.bottomSheetVerificationRecyclerView.cleanup()
controller.removeModelBuildListener(modelBuildListener)
controller.listener = null controller.listener = null
super.onDestroyView() super.onDestroyView()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.addModelBuildListener(modelBuildListener)
controller.listener = this controller.listener = this
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
// Timber.w("VALR: invalidate with State: ${state.pendingRequest}") // Timber.w("VALR: invalidate with State: ${state.pendingRequest}")
if (state.isNewScreen()) {
requestAccessibilityFocus = true
}
controller.update(state) controller.update(state)
} }
@ -176,4 +198,41 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
override fun declineRequest() { override fun declineRequest() {
viewModel.handle(VerificationAction.CancelPendingVerification) viewModel.handle(VerificationAction.CancelPendingVerification)
} }
private var currentScreenIndex = -1
private fun SelfVerificationViewState.isNewScreen(): Boolean {
val newIndex = toScreenIndex()
if (currentScreenIndex == newIndex) {
return false
}
currentScreenIndex = newIndex
return true
}
private fun SelfVerificationViewState.toScreenIndex(): Int {
return if (activeAction !is UserAction.None) {
when (activeAction) {
UserAction.ConfirmCancel -> 30
UserAction.None -> 31
}
} else {
when (pendingRequest) {
is Fail -> 0
is Loading -> 1
is Success -> when (pendingRequest.invoke().state) {
EVerificationState.WaitingForReady -> 10
EVerificationState.Requested -> 11
EVerificationState.Ready -> 12
EVerificationState.Started -> 13
EVerificationState.WeStarted -> 14
EVerificationState.WaitingForDone -> 15
EVerificationState.Done -> 16
EVerificationState.Cancelled -> 17
EVerificationState.HandledByOtherSession -> 18
}
Uninitialized -> 2
}
}
}
} }

View file

@ -21,12 +21,18 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
@ -36,6 +42,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.qrcode.QrCodeScannerActivity import im.vector.app.features.qrcode.QrCodeScannerActivity
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -45,6 +52,16 @@ class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChild
@Inject lateinit var controller: UserVerificationController @Inject lateinit var controller: UserVerificationController
private var requestAccessibilityFocus: Boolean = false
private val modelBuildListener: OnModelBuildFinishedListener = OnModelBuildFinishedListener {
if (requestAccessibilityFocus) {
// Do not use giveAccessibilityFocusOnce() here.
views.bottomSheetVerificationRecyclerView.layoutManager?.findViewByPosition(0)?.giveAccessibilityFocus()
requestAccessibilityFocus = false
// Note: it does not work when the recycler view is displayed for the first time, because findViewByPosition(0) is null
}
}
private val viewModel by parentFragmentViewModel(UserVerificationViewModel::class) private val viewModel by parentFragmentViewModel(UserVerificationViewModel::class)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding {
@ -58,17 +75,22 @@ class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChild
override fun onDestroyView() { override fun onDestroyView() {
views.bottomSheetVerificationRecyclerView.cleanup() views.bottomSheetVerificationRecyclerView.cleanup()
controller.removeModelBuildListener(modelBuildListener)
controller.listener = null controller.listener = null
super.onDestroyView() super.onDestroyView()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.addModelBuildListener(modelBuildListener)
controller.listener = this controller.listener = this
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
// Timber.w("VALR: invalidate with State: ${state.pendingRequest}") // Timber.w("VALR: invalidate with State: ${state.pendingRequest}")
if (state.isNewScreen()) {
requestAccessibilityFocus = true
}
controller.update(state) controller.update(state)
} }
@ -142,10 +164,40 @@ class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChild
} }
override fun onUserDeniesQrCodeScanned() { override fun onUserDeniesQrCodeScanned() {
viewModel.handle(VerificationAction.OtherUserDidNotScanned) viewModel.handle(VerificationAction.OtherUserDidNotScanned)
} }
override fun onUserConfirmsQrCodeScanned() { override fun onUserConfirmsQrCodeScanned() {
viewModel.handle(VerificationAction.OtherUserScannedSuccessfully) viewModel.handle(VerificationAction.OtherUserScannedSuccessfully)
} }
private var currentScreenIndex = -1
private fun UserVerificationViewState.isNewScreen(): Boolean {
val newIndex = toScreenIndex()
if (currentScreenIndex == newIndex) {
return false
}
currentScreenIndex = newIndex
return true
}
private fun UserVerificationViewState.toScreenIndex(): Int {
return when (pendingRequest) {
is Fail -> 0
is Loading -> 1
is Success -> when (pendingRequest.invoke().state) {
EVerificationState.WaitingForReady -> 10
EVerificationState.Requested -> 11
EVerificationState.Ready -> 12
EVerificationState.Started -> 13
EVerificationState.WeStarted -> 14
EVerificationState.WaitingForDone -> 15
EVerificationState.Done -> 16
EVerificationState.Cancelled -> 17
EVerificationState.HandledByOtherSession -> 18
}
Uninitialized -> 2
}
}
} }

View file

@ -35,13 +35,16 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import im.vector.app.R
import im.vector.app.core.contacts.MappedContact import im.vector.app.core.contacts.MappedContact
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.AvatarPlaceholder import im.vector.app.core.glide.AvatarPlaceholder
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequest
import im.vector.app.core.glide.GlideRequests import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import jp.wasabeef.glide.transformations.ColorFilterTransformation import jp.wasabeef.glide.transformations.ColorFilterTransformation
@ -58,7 +61,8 @@ import javax.inject.Inject
class AvatarRenderer @Inject constructor( class AvatarRenderer @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val matrixItemColorProvider: MatrixItemColorProvider, private val matrixItemColorProvider: MatrixItemColorProvider,
private val dimensionConverter: DimensionConverter private val dimensionConverter: DimensionConverter,
private val stringProvider: StringProvider,
) { ) {
companion object { companion object {
@ -67,6 +71,7 @@ class AvatarRenderer @Inject constructor(
@UiThread @UiThread
fun render(matrixItem: MatrixItem, imageView: ImageView) { fun render(matrixItem: MatrixItem, imageView: ImageView) {
imageView.setContentDescription(matrixItem)
render( render(
GlideApp.with(imageView), GlideApp.with(imageView),
matrixItem, matrixItem,
@ -100,6 +105,7 @@ class AvatarRenderer @Inject constructor(
@UiThread @UiThread
fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
imageView.setContentDescription(matrixItem)
render( render(
glideRequests, glideRequests,
matrixItem, matrixItem,
@ -109,6 +115,7 @@ class AvatarRenderer @Inject constructor(
@UiThread @UiThread
fun render(matrixItem: MatrixItem, localUri: Uri?, imageView: ImageView) { fun render(matrixItem: MatrixItem, localUri: Uri?, imageView: ImageView) {
imageView.setContentDescription(matrixItem)
val placeholder = getPlaceholderDrawable(matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
GlideApp.with(imageView) GlideApp.with(imageView)
.load(localUri?.let { File(localUri.path!!) }) .load(localUri?.let { File(localUri.path!!) })
@ -295,4 +302,28 @@ class AvatarRenderer @Inject constructor(
return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver()
?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
} }
/**
* Accessibility management.
*/
private fun ImageView.setContentDescription(matrixItem: MatrixItem) {
// Do not set contentDescription if the ImageView should be ignored regarding accessibility.
if (isImportantForAccessibility.not()) return
when (matrixItem) {
is MatrixItem.SpaceItem -> {
contentDescription = stringProvider.getString(R.string.avatar_of_space, matrixItem.getBestName())
}
is MatrixItem.RoomAliasItem,
is MatrixItem.RoomItem -> {
contentDescription = stringProvider.getString(R.string.avatar_of_room, matrixItem.getBestName())
}
is MatrixItem.UserItem -> {
contentDescription = stringProvider.getString(R.string.avatar_of_user, matrixItem.getBestName())
}
is MatrixItem.EveryoneInRoomItem,
is MatrixItem.EventItem -> {
// NA
}
}
}
} }

View file

@ -21,9 +21,12 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.View import android.view.View
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import androidx.core.view.ViewCompat
import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.Alerter
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.core.utils.isAnimationEnabled
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
@ -46,6 +49,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class PopupAlertManager @Inject constructor( class PopupAlertManager @Inject constructor(
private val clock: Clock, private val clock: Clock,
private val stringProvider: StringProvider,
) { ) {
companion object { companion object {
@ -282,6 +286,9 @@ class PopupAlertManager @Inject constructor(
} }
currentIsDismissed() currentIsDismissed()
} }
.setOnShowListener {
handleAccessibility(activity, animate)
}
.enableSwipeToDismiss() .enableSwipeToDismiss()
.enableInfiniteDuration(true) .enableInfiniteDuration(true)
.apply { .apply {
@ -297,6 +304,29 @@ class PopupAlertManager @Inject constructor(
.show() .show()
} }
/* a11y */
private fun handleAccessibility(activity: Activity, giveFocus: Boolean) {
activity.window.decorView.findViewById<View>(R.id.llAlertBackground)?.let { alertView ->
alertView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
// Add close action for a11y (same action than swipe). User can select the action by swiping on the screen vertically,
// and double tap to perform the action
ViewCompat.addAccessibilityAction(
alertView,
stringProvider.getString(R.string.action_close)
) { _, _ ->
currentIsDismissed()
Alerter.hide()
true
}
// And give focus to the alert right now, only for first display, i.e. when there is an animation.
if (giveFocus) {
alertView.giveAccessibilityFocus()
}
}
}
private fun currentIsDismissed() { private fun currentIsDismissed() {
// current alert has been hidden // current alert has been hidden
if (currentAlerter?.isLight == false) { if (currentAlerter?.isLight == false) {

View file

@ -43,4 +43,4 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -49,15 +49,14 @@
app:layout_constraintTop_toBottomOf="@id/syncStateView"> app:layout_constraintTop_toBottomOf="@id/syncStateView">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutMediumStyle"
android:id="@+id/collapsing_toolbar" android:id="@+id/collapsing_toolbar"
style="@style/Widget.Vector.Material3.CollapsingToolbar.Medium"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize" android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed"> app:layout_scrollFlags="scroll|exitUntilCollapsed">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
style="@style/Widget.Vector.Material3.Toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="0dp" android:elevation="0dp"

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:indeterminate="true" />
</RelativeLayout>