Yuito-app-android/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDele...

358 lines
14 KiB
Kotlin
Raw Normal View History

package com.keylesspalace.tusky.util
import android.content.Context
import android.os.Bundle
import android.text.Spannable
import android.text.style.URLSpan
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.entity.Status.Companion.MAX_MEDIA_ATTACHMENTS
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlin.math.min
// Not using lambdas because there's boxing of int then
fun interface StatusProvider {
fun getStatus(pos: Int): StatusViewData?
}
class ListStatusAccessibilityDelegate(
private val recyclerView: RecyclerView,
private val statusActionListener: StatusActionListener,
private val statusProvider: StatusProvider
) : RecyclerViewAccessibilityDelegate(recyclerView) {
private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
override fun getItemDelegate(): AccessibilityDelegateCompat = itemDelegate
private val context: Context get() = recyclerView.context
private val itemDelegate = object : RecyclerViewAccessibilityDelegate.ItemDelegate(this) {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
val pos = recyclerView.getChildAdapterPosition(host)
val status = statusProvider.getStatus(pos) ?: return
if (status is StatusViewData.Concrete) {
if (!status.spoilerText.isNullOrEmpty()) {
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(
host: View,
action: Int,
args: Bundle?
): Boolean {
val pos = recyclerView.getChildAdapterPosition(host)
when (action) {
R.id.action_reply -> {
interrupt()
statusActionListener.onReply(pos)
}
R.id.action_favourite -> statusActionListener.onFavourite(true, pos)
R.id.action_unfavourite -> statusActionListener.onFavourite(false, pos)
R.id.action_bookmark -> statusActionListener.onBookmark(true, pos)
R.id.action_unbookmark -> statusActionListener.onBookmark(false, pos)
R.id.action_reblog -> statusActionListener.onReblog(true, pos)
R.id.action_unreblog -> statusActionListener.onReblog(false, pos)
R.id.action_open_profile -> {
interrupt()
statusActionListener.onViewAccount(
(statusProvider.getStatus(pos) as StatusViewData.Concrete).actionable.account.id
)
}
R.id.action_open_media_1 -> {
interrupt()
statusActionListener.onViewMedia(pos, 0, null)
}
R.id.action_open_media_2 -> {
interrupt()
statusActionListener.onViewMedia(pos, 1, null)
}
R.id.action_open_media_3 -> {
interrupt()
statusActionListener.onViewMedia(pos, 2, null)
}
R.id.action_open_media_4 -> {
interrupt()
statusActionListener.onViewMedia(pos, 3, null)
}
R.id.action_expand_cw -> {
// Toggling it directly to avoid animations
// which cannot be disabled for detaild status for some reason
val holder = recyclerView.getChildViewHolder(host) as StatusBaseViewHolder
holder.toggleContentWarning()
// Stop and restart narrator before it reads old description.
// Would be nice if we could *just* read the content here but doesn't seem
// to be possible.
forceFocus(host)
}
R.id.action_collapse_cw -> {
statusActionListener.onExpandedChange(false, pos)
interrupt()
}
R.id.action_links -> showLinksDialog(host)
R.id.action_mentions -> showMentionsDialog(host)
R.id.action_hashtags -> showHashtagsDialog(host)
R.id.action_open_reblogger -> {
interrupt()
statusActionListener.onOpenReblog(pos)
}
R.id.action_open_reblogged_by -> {
interrupt()
statusActionListener.onShowReblogs(pos)
}
R.id.action_open_faved_by -> {
interrupt()
statusActionListener.onShowFavs(pos)
}
R.id.action_more -> {
statusActionListener.onMore(host, pos)
}
else -> return super.performAccessibilityAction(host, action, args)
}
return true
}
private fun showLinksDialog(host: View) {
val status = getStatus(host) as? StatusViewData.Concrete ?: return
val links = getLinks(status).toList()
val textLinks = links.map { item -> item.link }
AlertDialog.Builder(host.context)
.setTitle(R.string.title_links_dialog)
.setAdapter(
ArrayAdapter(
host.context,
android.R.layout.simple_list_item_1,
textLinks
)
) { _, which -> host.context.openLink(links[which].link) }
.show()
.let { forceFocus(it.listView) }
}
private fun showMentionsDialog(host: View) {
val status = getStatus(host) as? StatusViewData.Concrete ?: return
val mentions = status.actionable.mentions
val stringMentions = mentions.map { it.username }
AlertDialog.Builder(host.context)
.setTitle(R.string.title_mentions_dialog)
.setAdapter(
ArrayAdapter<CharSequence>(
host.context,
android.R.layout.simple_list_item_1, stringMentions
)
) { _, which ->
statusActionListener.onViewAccount(mentions[which].id)
}
.show()
.let { forceFocus(it.listView) }
}
private fun showHashtagsDialog(host: View) {
val status = getStatus(host) as? StatusViewData.Concrete ?: return
val tags = getHashtags(status).map { it.subSequence(1, it.length) }.toList()
AlertDialog.Builder(host.context)
.setTitle(R.string.title_hashtags_dialog)
.setAdapter(
ArrayAdapter(
host.context,
android.R.layout.simple_list_item_1, tags
)
) { _, which ->
statusActionListener.onViewTag(tags[which].toString())
}
.show()
.let { forceFocus(it.listView) }
}
private fun getStatus(childView: View): StatusViewData {
return statusProvider.getStatus(recyclerView.getChildAdapterPosition(childView))!!
}
}
private fun getLinks(status: StatusViewData.Concrete): Sequence<LinkSpanInfo> {
val content = status.content
return if (content is Spannable) {
content.getSpans(0, content.length, URLSpan::class.java)
.asSequence()
.map { span ->
val text = content.subSequence(
content.getSpanStart(span),
content.getSpanEnd(span)
)
if (isHashtag(text)) null else LinkSpanInfo(text.toString(), span.url)
}
.filterNotNull()
} else {
emptySequence()
}
}
private fun getHashtags(status: StatusViewData.Concrete): Sequence<CharSequence> {
val content = status.content
return content.getSpans(0, content.length, Object::class.java)
.asSequence()
.map { span ->
content.subSequence(content.getSpanStart(span), content.getSpanEnd(span))
}
.filter(this::isHashtag)
}
private fun forceFocus(host: View) {
interrupt()
host.post {
host.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
}
}
private fun interrupt() {
a11yManager.interrupt()
}
private fun isHashtag(text: CharSequence) = text.startsWith("#")
private val collapseCwAction = AccessibilityActionCompat(
R.id.action_collapse_cw,
context.getString(R.string.post_content_warning_show_less)
)
private val expandCwAction = AccessibilityActionCompat(
R.id.action_expand_cw,
context.getString(R.string.post_content_warning_show_more)
)
private val replyAction = AccessibilityActionCompat(
R.id.action_reply,
context.getString(R.string.action_reply)
)
private val unreblogAction = AccessibilityActionCompat(
R.id.action_unreblog,
context.getString(R.string.action_unreblog)
)
private val reblogAction = AccessibilityActionCompat(
R.id.action_reblog,
context.getString(R.string.action_reblog)
)
private val unfavouriteAction = AccessibilityActionCompat(
R.id.action_unfavourite,
context.getString(R.string.action_unfavourite)
)
private val favouriteAction = AccessibilityActionCompat(
R.id.action_favourite,
context.getString(R.string.action_favourite)
)
private val bookmarkAction = AccessibilityActionCompat(
R.id.action_bookmark,
context.getString(R.string.action_bookmark)
)
private val unbookmarkAction = AccessibilityActionCompat(
R.id.action_unbookmark,
context.getString(R.string.action_bookmark)
)
private val openProfileAction = AccessibilityActionCompat(
R.id.action_open_profile,
context.getString(R.string.action_view_profile)
)
private val linksAction = AccessibilityActionCompat(
R.id.action_links,
context.getString(R.string.action_links)
)
private val mentionsAction = AccessibilityActionCompat(
R.id.action_mentions,
context.getString(R.string.action_mentions)
)
private val hashtagsAction = AccessibilityActionCompat(
R.id.action_hashtags,
context.getString(R.string.action_hashtags)
)
private val openRebloggerAction = AccessibilityActionCompat(
R.id.action_open_reblogger,
context.getString(R.string.action_open_reblogger)
)
private val openRebloggedByAction = AccessibilityActionCompat(
R.id.action_open_reblogged_by,
context.getString(R.string.action_open_reblogged_by)
)
private val openFavsAction = AccessibilityActionCompat(
R.id.action_open_faved_by,
context.getString(R.string.action_open_faved_by)
)
private val moreAction = AccessibilityActionCompat(
R.id.action_more,
context.getString(R.string.action_more)
)
private data class LinkSpanInfo(val text: String, val link: String)
}