fix: Enable talkback actions in notifications and conversations (#661)

`ListStatusAccessiblityDelegate` was ignoring notifications because it
was checking the status against the concrete `StatusViewData` and not
the interface `IStatusViewData`.

Fix that and now notifications have accessibility actions.

`ConversationsFragment` didn't set the accessibility delegate, so no
actions appeared. Fix that so they do.
This commit is contained in:
Nik Clayton 2024-04-28 23:46:11 +02:00 committed by GitHub
parent b4780b146c
commit f771dd7026
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 46 deletions

View File

@ -55,6 +55,7 @@ import app.pachli.fragment.SFragment
import app.pachli.interfaces.ActionButtonActivity
import app.pachli.interfaces.ReselectableFragment
import app.pachli.interfaces.StatusActionListener
import app.pachli.util.ListStatusAccessibilityDelegate
import app.pachli.util.StatusDisplayOptionsRepository
import at.connyduck.sparkbutton.helpers.Utils
import com.google.android.material.color.MaterialColors
@ -220,6 +221,15 @@ class ConversationsFragment :
}
private fun setupRecyclerView() {
binding.recyclerView.setAccessibilityDelegateCompat(
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
if (pos in 0 until adapter.itemCount) {
adapter.peek(pos)
} else {
null
}
},
)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context)

View File

@ -26,8 +26,10 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@ -106,6 +108,8 @@ class NotificationsFragment :
private lateinit var layoutManager: LinearLayoutManager
private var talkBackWasEnabled = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -149,7 +153,11 @@ class NotificationsFragment :
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.setAccessibilityDelegateCompat(
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos: Int ->
adapter.snapshot().getOrNull(pos)
if (pos in 0 until adapter.itemCount) {
adapter.peek(pos)
} else {
null
}
},
)
binding.recyclerView.addItemDecoration(
@ -502,6 +510,14 @@ class NotificationsFragment :
override fun onResume() {
super.onResume()
val a11yManager = ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java)
val wasEnabled = talkBackWasEnabled
talkBackWasEnabled = a11yManager?.isEnabled == true
if (talkBackWasEnabled && !wasEnabled) {
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}
clearNotificationsForAccount(requireContext(), viewModel.account)
}

View File

@ -49,51 +49,50 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
val pos = recyclerView.getChildAdapterPosition(host)
val status = statusProvider.getStatus(pos) ?: return
if (status is StatusViewData) {
if (status.spoilerText.isNotEmpty()) {
info.addAction(if (status.isExpanded) collapseCwAction else expandCwAction)
}
info.addAction(replyAction)
val actionable = status.actionable
if (actionable.rebloggingAllowed()) {
info.addAction(if (actionable.reblogged) unreblogAction else reblogAction)
}
info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction)
info.addAction(if (actionable.bookmarked) unbookmarkAction else bookmarkAction)
val mediaActions = intArrayOf(
R.id.action_open_media_1,
R.id.action_open_media_2,
R.id.action_open_media_3,
R.id.action_open_media_4,
)
val attachmentCount = min(actionable.attachments.size, MAX_MEDIA_ATTACHMENTS)
for (i in 0 until attachmentCount) {
info.addAction(
AccessibilityActionCompat(
mediaActions[i],
context.getString(R.string.action_open_media_n, i + 1),
),
)
}
info.addAction(openProfileAction)
if (getLinks(status).any()) info.addAction(linksAction)
val mentions = actionable.mentions
if (mentions.isNotEmpty()) info.addAction(mentionsAction)
if (getHashtags(status).any()) info.addAction(hashtagsAction)
if (!status.status.reblog?.account?.username.isNullOrEmpty()) {
info.addAction(openRebloggerAction)
}
if (actionable.reblogsCount > 0) info.addAction(openRebloggedByAction)
if (actionable.favouritesCount > 0) info.addAction(openFavsAction)
info.addAction(moreAction)
if (status.spoilerText.isNotEmpty()) {
info.addAction(if (status.isExpanded) collapseCwAction else expandCwAction)
}
info.addAction(replyAction)
val actionable = status.actionable
if (actionable.rebloggingAllowed()) {
info.addAction(if (actionable.reblogged) unreblogAction else reblogAction)
}
info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction)
info.addAction(if (actionable.bookmarked) unbookmarkAction else bookmarkAction)
val mediaActions = intArrayOf(
R.id.action_open_media_1,
R.id.action_open_media_2,
R.id.action_open_media_3,
R.id.action_open_media_4,
)
val attachmentCount = min(actionable.attachments.size, MAX_MEDIA_ATTACHMENTS)
for (i in 0 until attachmentCount) {
info.addAction(
AccessibilityActionCompat(
mediaActions[i],
context.getString(R.string.action_open_media_n, i + 1),
),
)
}
info.addAction(openProfileAction)
if (getLinks(status).any()) info.addAction(linksAction)
val mentions = actionable.mentions
if (mentions.isNotEmpty()) info.addAction(mentionsAction)
if (getHashtags(status).any()) info.addAction(hashtagsAction)
if (!status.status.reblog?.account?.username.isNullOrEmpty()) {
info.addAction(openRebloggerAction)
}
if (actionable.reblogsCount > 0) info.addAction(openRebloggedByAction)
if (actionable.favouritesCount > 0) info.addAction(openFavsAction)
info.addAction(moreAction)
}
override fun performAccessibilityAction(
@ -230,7 +229,7 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
}
}
private fun getLinks(status: StatusViewData): Sequence<LinkSpanInfo> {
private fun getLinks(status: IStatusViewData): Sequence<LinkSpanInfo> {
val content = status.content
return if (content is Spannable) {
content.getSpans(0, content.length, URLSpan::class.java)
@ -248,7 +247,7 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
}
}
private fun getHashtags(status: StatusViewData): Sequence<CharSequence> {
private fun getHashtags(status: IStatusViewData): Sequence<CharSequence> {
val content = status.content
return content.getSpans(0, content.length, Object::class.java)
.asSequence()