[Android] [PATCH] Accessibility improvements for audio/video lists

aryan aryanchoudharyk2008 at gmail.com
Sun Jan 18 09:05:40 UTC 2026


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      | 126 ++++++++++++++++
 .../vlc/gui/browser/BaseBrowserAdapter.kt     | 127 ++++++++++++++++
 .../vlc/gui/video/VideoGridFragment.kt        |   4 +
 .../vlc/gui/video/VideoListAdapter.kt         | 136 ++++++++++++++++++
 5 files changed, 417 insertions(+)

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..e46a8d4f4 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
 
@@ -314,6 +332,7 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
                 }
             }
             binding.imageWidth = listImageWidth
+            binding.itemMore.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
         }
 
         override fun selectView(selected: Boolean) {
@@ -355,6 +374,7 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
                 }
             binding.imageWidth = cardSize
             binding.container.layoutParams.width = cardSize
+            binding.itemMore.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
 
         }
 
@@ -384,6 +404,112 @@ open class AudioBrowserAdapter @JvmOverloads constructor(
     @TargetApi(Build.VERSION_CODES.M)
     abstract inner class AbstractMediaItemViewHolder<T : ViewDataBinding>(binding: T) : SelectorViewHolder<T>(binding), MarqueeViewHolder {
 
+        init {
+             ViewCompat.setAccessibilityDelegate(itemView, object : AccessibilityDelegateCompat() {
+                override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+                    super.onInitializeAccessibilityNodeInfo(host, info)
+                    val position = layoutPosition
+                    if (!isPositionValid(position)) return
+                    val item = getItem(position) ?: return
+                    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 position = layoutPosition
+                    if (!isPositionValid(position)) return super.performAccessibilityAction(host, action, args)
+                    val item = getItem(position) ?: return super.performAccessibilityAction(host, action, args)
+
+                    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) {
+                        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..e949db439 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 {
@@ -275,6 +290,118 @@ open class BaseBrowserAdapter(val browserContainer: BrowserContainer<MediaLibrar
 
             bindingContainer.banIcon.onFocusChangeListener = focusChangeListener
             bindingContainer.container.onFocusChangeListener = focusChangeListener
+            bindingContainer.moreIcon.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+
+            ViewCompat.setAccessibilityDelegate(itemView, object : AccessibilityDelegateCompat() {
+                override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+                    super.onInitializeAccessibilityNodeInfo(host, info)
+                    val position = layoutPosition
+                    if (position < 0 || position >= dataset.size) return
+                    val item = dataset[position]
+                    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 position = layoutPosition
+                    if (position < 0 || position >= dataset.size) return super.performAccessibilityAction(host, action, args)
+                    val item = dataset[position]
+
+                    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) {
+                        (browserContainer as CtxActionReceiver).onCtxAction(position, option)
+                        return true
+                    }
+
+                    return super.performAccessibilityAction(host, action, args)
+                }
+            })
         }
 
         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..59c18e2be 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"
 
@@ -201,11 +212,136 @@ class VideoListAdapter(private var isSeenMediaMarkerVisible: Boolean, private va
         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)
                     true
                 }
+            ViewCompat.setAccessibilityDelegate(itemView, object : AccessibilityDelegateCompat() {
+                override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+                    super.onInitializeAccessibilityNodeInfo(host, info)
+                    val position = layoutPosition
+                    if (!isPositionValid(position)) return
+                    val item = getItem(position) ?: return
+                    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 position = layoutPosition
+                    if (!isPositionValid(position)) return super.performAccessibilityAction(host, action, args)
+                    val item = getItem(position) ?: return super.performAccessibilityAction(host, action, args)
+
+                    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) {
+                        eventsChannel.trySend(VideoCtxAction(position, option))
+                        return true
+                    }
+
+                    return super.performAccessibilityAction(host, action, args)
+                }
+            })
         }
 
         fun onImageClick(@Suppress("UNUSED_PARAMETER") v: View) {
-- 
2.52.0.windows.1




More information about the Android mailing list