[Android] [PATCH v2] Accessibility: Add custom actions to lists (play, Favorite, etc.)
aryan
aryanchoudharyk2008 at gmail.com
Sun Jan 18 10:13:37 UTC 2026
Changes in v2:
- Moved AccessibilityDelegate to onBindViewHolder to fix actions disappearing on scroll.
Hello VideoLAN Android Team,
I am submitting a patch to improve the TalkBack experience in the media browser and player lists.
The Problem: Currently, list items require excessive swiping to access common actions (like "More options"), and the interface does not expose quick actions like "Remove from favorites" or "Seek" directly to accessibility services.
The Fix: This patch implements AccessibilityDelegateCompat to:
1.
Add Custom Accessibility Actions (Seek, Favorite, Play Next, etc.) directly to the list items.
2.
Hide the redundant "More" button from TalkBack focus (IMPORTANT_FOR_ACCESSIBILITY_NO) to reduce clutter.
3.
Dynamically update action labels (e.g., swapping "Add to favorites" with "Remove from favorites" based on state).
I am submitting this via the mailing list as I am unable to create an account on code.videolan.org at the moment.
Best regards, Aryan
---
.../resources/src/main/res/values/ids.xml | 24 +++
.../vlc/gui/audio/AudioBrowserAdapter.kt | 127 +++++++++++++++
.../vlc/gui/browser/BaseBrowserAdapter.kt | 132 ++++++++++++++++
.../vlc/gui/video/VideoGridFragment.kt | 4 +
.../vlc/gui/video/VideoListAdapter.kt | 146 +++++++++++++++++-
5 files changed, 432 insertions(+), 1 deletion(-)
diff --git a/application/resources/src/main/res/values/ids.xml b/application/resources/src/main/res/values/ids.xml
index 5ff0ec18e..59ad7b7ab 100644
--- a/application/resources/src/main/res/values/ids.xml
+++ b/application/resources/src/main/res/values/ids.xml
@@ -25,4 +25,28 @@
<resources>
<item type="id" name="sort" />
<item type="id" name="sort_desc" />
+ <item type="id" name="accessibility_action_append" />
+ <item type="id" name="accessibility_action_play_next" />
+ <item type="id" name="accessibility_action_delete" />
+ <item type="id" name="accessibility_action_add_to_playlist" />
+ <item type="id" name="accessibility_action_info" />
+ <item type="id" name="accessibility_action_play_all" />
+ <item type="id" name="accessibility_action_share" />
+ <item type="id" name="accessibility_action_set_ringtone" />
+ <item type="id" name="accessibility_action_download_subtitles" />
+ <item type="id" name="accessibility_action_play_as_audio" />
+ <item type="id" name="accessibility_action_add_favorite" />
+ <item type="id" name="accessibility_action_remove_favorite" />
+ <item type="id" name="accessibility_action_mark_played" />
+ <item type="id" name="accessibility_action_mark_unplayed" />
+ <item type="id" name="accessibility_action_add_shortcut" />
+ <item type="id" name="accessibility_action_go_to_folder" />
+ <item type="id" name="accessibility_action_add_group" />
+ <item type="id" name="accessibility_action_group_similar" />
+ <item type="id" name="accessibility_action_rename" />
+ <item type="id" name="accessibility_action_play_from_start" />
+ <item type="id" name="accessibility_action_go_to_album" />
+ <item type="id" name="accessibility_action_go_to_artist" />
+ <item type="id" name="accessibility_action_ban_folder" />
+ <item type="id" name="accessibility_action_open_context_menu" />
</resources>
\ No newline at end of file
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioBrowserAdapter.kt b/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioBrowserAdapter.kt
index a8be20509..33572698e 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioBrowserAdapter.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioBrowserAdapter.kt
@@ -41,9 +41,11 @@ import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.videolan.libvlc.util.AndroidUtil
+import org.videolan.medialibrary.interfaces.media.Album
import org.videolan.medialibrary.interfaces.media.Artist
import org.videolan.medialibrary.interfaces.media.Genre
import org.videolan.medialibrary.interfaces.media.MediaWrapper
+import org.videolan.medialibrary.interfaces.media.Playlist
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.medialibrary.media.MediaLibraryItem.FLAG_SELECTED
import org.videolan.resources.AppContextProvider
@@ -73,6 +75,22 @@ import org.videolan.vlc.util.isOTG
import org.videolan.vlc.util.isSD
import org.videolan.vlc.util.isSchemeSMB
import org.videolan.vlc.viewmodels.PlaylistModel
+import android.os.Bundle
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
+import org.videolan.vlc.media.MediaUtils
+import org.videolan.vlc.gui.helpers.UiTools
+import org.videolan.vlc.gui.helpers.UiTools.addToPlaylist
+import org.videolan.vlc.gui.helpers.UiTools.showMediaInfo
+import org.videolan.vlc.util.share
+import androidx.lifecycle.lifecycleScope
+import org.videolan.vlc.util.ContextOption
+import org.videolan.vlc.gui.dialogs.CtxActionReceiver
+import org.videolan.vlc.media.getAll
+import org.videolan.tools.retrieveParent
private const val SHOW_IN_LIST = -1
@@ -172,6 +190,7 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
if (position >= itemCount) return
val item = getItem(position)
holder.setItem(item)
+ holder.updateAccessibilityDelegate(item)
if (item is Artist) item.description = holder.binding.root.context.resources.getQuantityString(R.plurals.albums_quantity, item.albumsCount, item.albumsCount)
if (item is Genre) item.description = holder.binding.root.context.resources.getQuantityString(R.plurals.track_quantity, item.tracksCount, item.tracksCount)
val isSelected = multiSelectHelper.isSelected(position)
@@ -314,6 +333,7 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
}
}
binding.imageWidth = listImageWidth
+ binding.itemMore.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
override fun selectView(selected: Boolean) {
@@ -355,6 +375,7 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
}
binding.imageWidth = cardSize
binding.container.layoutParams.width = cardSize
+ binding.itemMore.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
@@ -384,6 +405,112 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
@TargetApi(Build.VERSION_CODES.M)
abstract inner class AbstractMediaItemViewHolder<T : ViewDataBinding>(binding: T) : SelectorViewHolder<T>(binding), MarqueeViewHolder {
+ fun updateAccessibilityDelegate(item: MediaLibraryItem?) {
+ if (item == null) {
+ ViewCompat.setAccessibilityDelegate(itemView, null)
+ return
+ }
+ ViewCompat.setAccessibilityDelegate(itemView, object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ val context = itemView.context
+
+ if (item.isFavorite) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_remove_favorite, context.getString(R.string.favorites_remove)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_favorite, context.getString(R.string.favorites_add)))
+ }
+
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_append, context.getString(R.string.append)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_next, context.getString(R.string.insert_next)))
+
+ if (item is MediaWrapper || item is Playlist) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_shortcut, context.getString(R.string.create_shortcut)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_rename, context.getString(R.string.rename)))
+ }
+
+ if (item is MediaWrapper) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_all, context.getString(R.string.play_all)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_info, context.getString(R.string.info)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_share, context.getString(R.string.share)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_to_playlist, context.getString(R.string.add_to_playlist)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_delete, context.getString(R.string.delete)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_set_ringtone, context.getString(R.string.set_song)))
+ if (item.uri.retrieveParent() != null) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_go_to_folder, context.getString(R.string.go_to_folder)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_go_to_album, context.getString(R.string.go_to_album)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_go_to_artist, context.getString(R.string.go_to_artist)))
+ } else if (item is Album || item is Artist || item is Genre || item is Playlist) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_all, context.getString(R.string.play_all)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_to_playlist, context.getString(R.string.add_to_playlist)))
+ if (item !is Genre) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_delete, context.getString(R.string.delete)))
+ }
+ if (item is Album) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_go_to_artist, context.getString(R.string.go_to_artist)))
+ }
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_open_context_menu, context.getString(R.string.more_actions)))
+ }
+
+ override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
+ val option = when (action) {
+ R.id.accessibility_action_append -> ContextOption.CTX_APPEND
+ R.id.accessibility_action_play_next -> ContextOption.CTX_PLAY_NEXT
+ R.id.accessibility_action_info -> ContextOption.CTX_INFORMATION
+ R.id.accessibility_action_share -> ContextOption.CTX_SHARE
+ R.id.accessibility_action_add_to_playlist -> ContextOption.CTX_ADD_TO_PLAYLIST
+ R.id.accessibility_action_delete -> ContextOption.CTX_DELETE
+ R.id.accessibility_action_set_ringtone -> ContextOption.CTX_SET_RINGTONE
+ R.id.accessibility_action_play_all -> ContextOption.CTX_PLAY_ALL
+ R.id.accessibility_action_add_favorite -> ContextOption.CTX_FAV_ADD
+ R.id.accessibility_action_remove_favorite -> ContextOption.CTX_FAV_REMOVE
+ R.id.accessibility_action_add_shortcut -> ContextOption.CTX_ADD_SHORTCUT
+ R.id.accessibility_action_go_to_folder -> ContextOption.CTX_GO_TO_FOLDER
+ R.id.accessibility_action_rename -> ContextOption.CTX_RENAME
+ R.id.accessibility_action_go_to_album -> ContextOption.CTX_GO_TO_ALBUM
+ R.id.accessibility_action_go_to_artist -> ContextOption.CTX_GO_TO_ARTIST
+ R.id.accessibility_action_open_context_menu -> {
+ onMoreClick(host)
+ return true
+ }
+ else -> null
+ }
+
+ if (option != null && eventsHandler is CtxActionReceiver) {
+ val position = layoutPosition
+ if (isPositionValid(position)) {
+ eventsHandler.onCtxAction(position, option)
+ }
+ return true
+ }
+
+ return super.performAccessibilityAction(host, action, args)
+ }
+ })
+ }
+
val canBeReordered: Boolean
get() = reorderable && !stopReorder
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserAdapter.kt b/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserAdapter.kt
index 96e4803e1..33ae5aa35 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserAdapter.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserAdapter.kt
@@ -55,6 +55,21 @@ import org.videolan.vlc.gui.view.MiniVisualizer
import org.videolan.vlc.util.LifecycleAwareScheduler
import org.videolan.vlc.util.getDescriptionSpan
import org.videolan.vlc.viewmodels.PlaylistModel
+import android.os.Bundle
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
+import org.videolan.vlc.media.MediaUtils
+import org.videolan.vlc.gui.helpers.UiTools
+import org.videolan.vlc.gui.helpers.UiTools.addToPlaylist
+import org.videolan.vlc.gui.helpers.UiTools.showMediaInfo
+import org.videolan.vlc.util.share
+import androidx.lifecycle.lifecycleScope
+import org.videolan.vlc.util.ContextOption
+import org.videolan.vlc.gui.dialogs.CtxActionReceiver
+import org.videolan.tools.retrieveParent
const val UPDATE_PROGRESS = "update_progress"
open class BaseBrowserAdapter(val browserContainer: BrowserContainer<MediaLibraryItem>, var sort:Int = Medialibrary.SORT_FILENAME, var asc:Boolean = true, val forMain:Boolean = true) : DiffUtilAdapter<MediaLibraryItem, BaseBrowserAdapter.ViewHolder<ViewDataBinding>>(), MultiSelectAdapter<MediaLibraryItem>, FastScroller.SeparatedAdapter {
@@ -155,6 +170,9 @@ open class BaseBrowserAdapter(val browserContainer: BrowserContainer<MediaLibrar
}
override fun onBindViewHolder(holder: ViewHolder<ViewDataBinding>, position: Int) {
+ if (holder is MediaViewHolder) {
+ holder.updateAccessibilityDelegate(dataset[position])
+ }
val viewType = getItemViewType(position)
if (viewType == TYPE_MEDIA) {
onBindMediaViewHolder(holder as MediaViewHolder, position)
@@ -258,6 +276,119 @@ open class BaseBrowserAdapter(val browserContainer: BrowserContainer<MediaLibrar
override val titleView: TextView = bindingContainer.title
var job : Job? = null
+ fun updateAccessibilityDelegate(item: MediaLibraryItem?) {
+ if (item == null) {
+ ViewCompat.setAccessibilityDelegate(itemView, null)
+ return
+ }
+ ViewCompat.setAccessibilityDelegate(itemView, object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ val context = itemView.context
+
+ if (item.isFavorite) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_remove_favorite, context.getString(R.string.favorites_remove)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_favorite, context.getString(R.string.favorites_add)))
+ }
+
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_append, context.getString(R.string.append)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_next, context.getString(R.string.insert_next)))
+
+ if (item is MediaWrapper) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_shortcut, context.getString(R.string.create_shortcut)))
+ if (item.uri.retrieveParent() != null) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_go_to_folder, context.getString(R.string.go_to_folder)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_rename, context.getString(R.string.rename)))
+ }
+
+ if (item is MediaWrapper) {
+ if (item.type != MediaWrapper.TYPE_DIR) {
+ if (item.playCount > 0) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_mark_unplayed, context.getString(R.string.mark_as_not_played)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_mark_played, context.getString(R.string.mark_as_played)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_all, context.getString(R.string.play_all)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_info, context.getString(R.string.info)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_share, context.getString(R.string.share)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_to_playlist, context.getString(R.string.add_to_playlist)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_delete, context.getString(R.string.delete)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_all, context.getString(R.string.play_all)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_to_playlist, context.getString(R.string.add_to_playlist)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_ban_folder, context.getString(R.string.ban_folder)))
+ }
+ if (item.type == MediaWrapper.TYPE_VIDEO) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_download_subtitles, context.getString(R.string.download_subtitles)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_as_audio, context.getString(R.string.play_as_audio)))
+ }
+ } else if (item is Storage) {
+ // Storage actions if any
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_open_context_menu, context.getString(R.string.more_actions)))
+ }
+
+ override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
+ val option = when (action) {
+ R.id.accessibility_action_add_favorite -> ContextOption.CTX_FAV_ADD
+ R.id.accessibility_action_remove_favorite -> ContextOption.CTX_FAV_REMOVE
+ R.id.accessibility_action_mark_played -> ContextOption.CTX_MARK_AS_PLAYED
+ R.id.accessibility_action_mark_unplayed -> ContextOption.CTX_MARK_AS_UNPLAYED
+ R.id.accessibility_action_append -> ContextOption.CTX_APPEND
+ R.id.accessibility_action_play_next -> ContextOption.CTX_PLAY_NEXT
+ R.id.accessibility_action_info -> ContextOption.CTX_INFORMATION
+ R.id.accessibility_action_share -> ContextOption.CTX_SHARE
+ R.id.accessibility_action_add_to_playlist -> ContextOption.CTX_ADD_TO_PLAYLIST
+ R.id.accessibility_action_delete -> ContextOption.CTX_DELETE
+ R.id.accessibility_action_download_subtitles -> ContextOption.CTX_DOWNLOAD_SUBTITLES
+ R.id.accessibility_action_play_as_audio -> ContextOption.CTX_PLAY_AS_AUDIO
+ R.id.accessibility_action_play_all -> ContextOption.CTX_PLAY_ALL
+ R.id.accessibility_action_add_shortcut -> ContextOption.CTX_ADD_SHORTCUT
+ R.id.accessibility_action_go_to_folder -> ContextOption.CTX_GO_TO_FOLDER
+ R.id.accessibility_action_rename -> ContextOption.CTX_RENAME
+ R.id.accessibility_action_ban_folder -> ContextOption.CTX_BAN_FOLDER
+ R.id.accessibility_action_open_context_menu -> {
+ onMoreClick(host)
+ return true
+ }
+ else -> null
+ }
+
+ if (option != null && browserContainer is CtxActionReceiver) {
+ val position = layoutPosition
+ if (position >= 0 && position < dataset.size) {
+ (browserContainer as CtxActionReceiver).onCtxAction(position, option)
+ }
+ return true
+ }
+
+ return super.performAccessibilityAction(host, action, args)
+ }
+ })
+ }
+
init {
bindingContainer.setHolder(this)
if (AndroidUtil.isMarshMallowOrLater) itemView.setOnContextClickListener { v ->
@@ -275,6 +406,7 @@ open class BaseBrowserAdapter(val browserContainer: BrowserContainer<MediaLibrar
bindingContainer.banIcon.onFocusChangeListener = focusChangeListener
bindingContainer.container.onFocusChangeListener = focusChangeListener
+ bindingContainer.moreIcon.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
override fun selectView(selected: Boolean) {
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/video/VideoGridFragment.kt b/application/vlc-android/src/org/videolan/vlc/gui/video/VideoGridFragment.kt
index 99092d919..8a856b977 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/video/VideoGridFragment.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/video/VideoGridFragment.kt
@@ -746,6 +746,9 @@ class VideoGridFragment : MediaBrowserFragment<VideosViewModel>(), SwipeRefreshL
onLongClick(position)
}
}
+ is VideoCtxAction -> {
+ onCtxAction(position, option)
+ }
}
}
@@ -806,3 +809,4 @@ class VideoClick(val position: Int, val item: MediaLibraryItem) : VideoAction()
class VideoLongClick(val position: Int, val item: MediaLibraryItem) : VideoAction()
class VideoCtxClick(val position: Int, val item: MediaLibraryItem) : VideoAction()
class VideoImageClick(val position: Int, val item: MediaLibraryItem) : VideoAction()
+class VideoCtxAction(val position: Int, val option: ContextOption) : VideoAction()
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/video/VideoListAdapter.kt b/application/vlc-android/src/org/videolan/vlc/gui/video/VideoListAdapter.kt
index 9e3a48292..1cb0602bb 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/video/VideoListAdapter.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/video/VideoListAdapter.kt
@@ -53,6 +53,17 @@ import org.videolan.vlc.media.isOTG
import org.videolan.vlc.media.isSD
import org.videolan.vlc.util.*
import org.videolan.vlc.viewmodels.mobile.VideoGroupingType
+import android.os.Bundle
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
+import org.videolan.vlc.media.MediaUtils
+import org.videolan.vlc.gui.helpers.UiTools.addToPlaylist
+import org.videolan.vlc.gui.helpers.UiTools.showMediaInfo
+import androidx.lifecycle.lifecycleScope
+import org.videolan.tools.retrieveParent
private const val TAG = "VLC/VideoListAdapter"
@@ -88,7 +99,12 @@ class VideoListAdapter(private var isSeenMediaMarkerVisible: Boolean, private va
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val item = getItem(position) ?: return
+ val item = getItem(position)
+ if (item == null) {
+ holder.updateAccessibilityDelegate(null)
+ return
+ }
+ holder.updateAccessibilityDelegate(item)
holder.binding.setVariable(BR.scaleType, ImageView.ScaleType.CENTER_CROP)
fillView(holder, item)
holder.binding.setVariable(BR.media, item)
@@ -198,9 +214,137 @@ class VideoListAdapter(private var isSeenMediaMarkerVisible: Boolean, private va
val title : TextView = itemView.findViewById(R.id.ml_item_title)
val more : ImageView = itemView.findViewById(R.id.item_more)
+ fun updateAccessibilityDelegate(item: MediaLibraryItem?) {
+ if (item == null) {
+ ViewCompat.setAccessibilityDelegate(itemView, null)
+ return
+ }
+ ViewCompat.setAccessibilityDelegate(itemView, object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ val context = itemView.context
+
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_append, context.getString(R.string.append)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_next, context.getString(R.string.insert_next)))
+
+ if (item.isFavorite) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_remove_favorite, context.getString(R.string.favorites_remove)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_favorite, context.getString(R.string.favorites_add)))
+ }
+
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_shortcut, context.getString(R.string.create_shortcut)))
+
+ if (item is MediaWrapper) {
+ if (item.seen > 0) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_mark_unplayed, context.getString(R.string.mark_as_not_played)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_mark_played, context.getString(R.string.mark_as_played)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_all, context.getString(R.string.play_all)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_as_audio, context.getString(R.string.play_as_audio)))
+ if (item.time > 0L) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_from_start, context.getString(R.string.play_from_start)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_info, context.getString(R.string.info)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_share, context.getString(R.string.share)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_to_playlist, context.getString(R.string.add_to_playlist)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_delete, context.getString(R.string.delete)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_rename, context.getString(R.string.rename)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_download_subtitles, context.getString(R.string.download_subtitles)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_group, context.getString(R.string.add_to_group)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_group_similar, context.getString(R.string.group_similar)))
+ if (item.uri.retrieveParent() != null) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_go_to_folder, context.getString(R.string.go_to_folder)))
+ }
+ } else if (item is VideoGroup || item is Folder) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_mark_unplayed, context.getString(R.string.mark_all_as_not_played)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_mark_played, context.getString(R.string.mark_all_as_played)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_play_all, context.getString(R.string.play_all)))
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_add_to_playlist, context.getString(R.string.add_to_playlist)))
+ if (item is Folder) {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_ban_folder, context.getString(R.string.ban_folder)))
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_rename, context.getString(R.string.rename_group)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_delete, context.getString(R.string.delete)))
+ }
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.accessibility_action_open_context_menu, context.getString(R.string.more_actions)))
+ }
+
+ override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
+ val option = when (action) {
+ R.id.accessibility_action_append -> ContextOption.CTX_APPEND
+ R.id.accessibility_action_play_next -> ContextOption.CTX_PLAY_NEXT
+ R.id.accessibility_action_info -> ContextOption.CTX_INFORMATION
+ R.id.accessibility_action_share -> ContextOption.CTX_SHARE
+ R.id.accessibility_action_add_to_playlist -> ContextOption.CTX_ADD_TO_PLAYLIST
+ R.id.accessibility_action_delete -> ContextOption.CTX_DELETE
+ R.id.accessibility_action_download_subtitles -> ContextOption.CTX_DOWNLOAD_SUBTITLES
+ R.id.accessibility_action_play_as_audio -> ContextOption.CTX_PLAY_AS_AUDIO
+ R.id.accessibility_action_play_from_start -> ContextOption.CTX_PLAY_FROM_START
+ R.id.accessibility_action_play_all -> ContextOption.CTX_PLAY_ALL
+ R.id.accessibility_action_add_favorite -> ContextOption.CTX_FAV_ADD
+ R.id.accessibility_action_remove_favorite -> ContextOption.CTX_FAV_REMOVE
+ R.id.accessibility_action_add_shortcut -> ContextOption.CTX_ADD_SHORTCUT
+ R.id.accessibility_action_add_group -> ContextOption.CTX_ADD_GROUP
+ R.id.accessibility_action_group_similar -> ContextOption.CTX_GROUP_SIMILAR
+ R.id.accessibility_action_go_to_folder -> ContextOption.CTX_GO_TO_FOLDER
+ R.id.accessibility_action_rename -> if (item is VideoGroup) ContextOption.CTX_RENAME_GROUP else ContextOption.CTX_RENAME
+ R.id.accessibility_action_ban_folder -> ContextOption.CTX_BAN_FOLDER
+ R.id.accessibility_action_mark_played -> if (item is MediaWrapper || item.itemType == MediaLibraryItem.TYPE_MEDIA) ContextOption.CTX_MARK_AS_PLAYED else ContextOption.CTX_MARK_ALL_AS_PLAYED
+ R.id.accessibility_action_mark_unplayed -> if (item is MediaWrapper || item.itemType == MediaLibraryItem.TYPE_MEDIA) ContextOption.CTX_MARK_AS_UNPLAYED else ContextOption.CTX_MARK_ALL_AS_UNPLAYED
+ R.id.accessibility_action_open_context_menu -> {
+ onMoreClick(host)
+ return true
+ }
+ else -> null
+ }
+
+ if (option != null) {
+ val position = layoutPosition
+ if (isPositionValid(position)) {
+ eventsChannel.trySend(VideoCtxAction(position, option))
+ }
+ return true
+ }
+
+ return super.performAccessibilityAction(host, action, args)
+ }
+ })
+ }
+
init {
binding.setVariable(BR.holder, this)
binding.setVariable(BR.cover, UiTools.getDefaultVideoDrawable(itemView.context))
+ more.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
if (AndroidUtil.isMarshMallowOrLater)
itemView.setOnContextClickListener { v ->
onMoreClick(v)
--
2.52.0.windows.1
More information about the Android
mailing list