From c29ae0e885a9cb4057c52716731fc43260d54d43 Mon Sep 17 00:00:00 2001 From: aryan Date: Sat, 27 Dec 2025 11:20:46 +0530 Subject: [PATCH 1/5] Improve accessibility for video lists and player details --- .../holder/StreamMiniInfoItemHolder.java | 215 +++++++++++++++++- .../layout/activity_player_queue_control.xml | 16 +- .../res/layout/dialog_feed_group_create.xml | 4 +- .../main/res/layout/fragment_video_detail.xml | 6 +- app/src/main/res/layout/list_stream_item.xml | 1 + .../main/res/layout/list_stream_mini_item.xml | 1 + app/src/main/res/values/ids.xml | 15 ++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/values/ids.xml diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 642738630..6d3c99307 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -1,27 +1,51 @@ package org.schabi.newpipe.info_list.holder; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.download.DownloadDialog; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.ktx.ViewUtils; +import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.SparseItemUtil; import org.schabi.newpipe.util.StreamTypeUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.image.CoilHelper; import org.schabi.newpipe.views.AnimatedProgressBar; +import java.util.List; import java.util.concurrent.TimeUnit; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; + public class StreamMiniInfoItemHolder extends InfoItemHolder { public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; @@ -57,7 +81,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { if (item.getDuration() > 0) { itemDurationView.setText(Localization.getDurationString(item.getDuration())); - itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + itemDurationView.setBackgroundColor(ContextCompat.getColor( + itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); @@ -76,7 +101,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } } else if (StreamTypeUtil.isLiveStream(item.getStreamType())) { itemDurationView.setText(R.string.duration_live); - itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + itemDurationView.setBackgroundColor(ContextCompat.getColor( + itemBuilder.getContext(), R.color.live_duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.GONE); @@ -145,10 +171,195 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } return true; }); + + updateAccessibilityActions(item); + } + + private void updateAccessibilityActions(final StreamInfoItem item) { + ViewCompat.setAccessibilityDelegate(itemView, new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo(final View host, + final AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + final Context context = itemBuilder.getContext(); + if (context == null) { + return; + } + + final PlayerHolder holder = PlayerHolder.INSTANCE; + if (holder.isPlayQueueReady()) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_enqueue, + context.getString(R.string.enqueue_stream))); + + if (holder.getQueuePosition() < holder.getQueueSize() - 1) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_enqueue_next, + context.getString(R.string.enqueue_next_stream))); + } + } + + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_background, + context.getString(R.string.start_here_on_background))); + + if (!StreamTypeUtil.isAudio(item.getStreamType())) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_popup, + context.getString(R.string.start_here_on_popup))); + } + + if (context instanceof FragmentActivity) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_download, + context.getString(R.string.download))); + + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_playlist, + context.getString(R.string.add_to_playlist))); + } + + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_share, + context.getString(R.string.share))); + + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_browser, + context.getString(R.string.open_in_browser))); + + if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_kodi, + context.getString(R.string.play_with_kodi_title))); + } + + final boolean isWatchHistoryEnabled = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.enable_watch_history_key), false); + if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(item.getStreamType())) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_mark_watched, + context.getString(R.string.mark_as_watched))); + } + + if (context instanceof AppCompatActivity) { + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_channel_details, + context.getString(R.string.show_channel_details))); + } + + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_show_options, + context.getString(R.string.more_options))); + } + + @Override + public boolean performAccessibilityAction(final View host, final int action, + final Bundle args) { + final Context context = itemBuilder.getContext(); + if (context == null) { + return super.performAccessibilityAction(host, action, args); + } + + if (action == R.id.accessibility_action_show_options) { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().held(item); + } + return true; + } else if (action == R.id.accessibility_action_enqueue) { + SparseItemUtil.fetchItemInfoIfSparse(context, item, + singlePlayQueue -> NavigationHelper.enqueueOnPlayer( + context, singlePlayQueue)); + return true; + } else if (action == R.id.accessibility_action_enqueue_next) { + SparseItemUtil.fetchItemInfoIfSparse(context, item, + singlePlayQueue -> NavigationHelper.enqueueNextOnPlayer( + context, singlePlayQueue)); + return true; + } else if (action == R.id.accessibility_action_background) { + SparseItemUtil.fetchItemInfoIfSparse(context, item, singlePlayQueue -> + NavigationHelper.playOnBackgroundPlayer( + context, singlePlayQueue, true)); + return true; + } else if (action == R.id.accessibility_action_popup) { + SparseItemUtil.fetchItemInfoIfSparse(context, item, singlePlayQueue -> + NavigationHelper.playOnPopupPlayer( + context, singlePlayQueue, true)); + return true; + } else if (action == R.id.accessibility_action_download) { + SparseItemUtil.fetchStreamInfoAndSaveToDatabase(context, + item.getServiceId(), + item.getUrl(), info -> { + final FragmentActivity activity = (FragmentActivity) context; + if (!activity.isFinishing() && !activity.isDestroyed()) { + final DownloadDialog downloadDialog = + new DownloadDialog(context, info); + downloadDialog.show(activity.getSupportFragmentManager(), + "downloadDialog"); + } + }); + return true; + } else if (action == R.id.accessibility_action_playlist) { + final FragmentActivity activity = (FragmentActivity) context; + PlaylistDialog.createCorrespondingDialog( + context, + List.of(new StreamEntity(item)), + dialog -> dialog.show( + activity.getSupportFragmentManager(), + "StreamDialogEntry@" + + (dialog instanceof PlaylistAppendDialog + ? "append" : "create") + + "_playlist" + ) + ); + return true; + } else if (action == R.id.accessibility_action_share) { + ShareUtils.shareText(context, item.getName(), + item.getUrl(), item.getThumbnails()); + return true; + } else if (action == R.id.accessibility_action_browser) { + ShareUtils.openUrlInBrowser(context, item.getUrl()); + return true; + } else if (action == R.id.accessibility_action_kodi) { + KoreUtils.playWithKore(context, Uri.parse(item.getUrl())); + return true; + } else if (action == R.id.accessibility_action_mark_watched) { + new HistoryRecordManager(context) + .markAsWatched(item) + .doOnError(error -> { + ErrorUtil.showSnackbar( + context, + new ErrorInfo( + error, + UserAction.OPEN_INFO_ITEM_DIALOG, + "Got an error when trying to mark as watched" + ) + ); + }) + .onErrorComplete() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + return true; + } else if (action == R.id.accessibility_action_channel_details) { + SparseItemUtil.fetchUploaderUrlIfSparse((AppCompatActivity) context, + item.getServiceId(), item.getUrl(), + item.getUploaderUrl(), + url -> NavigationHelper.openChannelFragment( + ((AppCompatActivity) context).getSupportFragmentManager(), + item.getServiceId(), url, item.getUploaderName())); + return true; + } + + return super.performAccessibilityAction(host, action, args); + } + }); } private void disableLongClick() { itemView.setLongClickable(false); itemView.setOnLongClickListener(null); + ViewCompat.setAccessibilityDelegate(itemView, null); } } + diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index 29efa36f9..d2961bf5f 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -176,7 +176,7 @@ android:scaleType="fitXY" android:src="@drawable/ic_repeat" android:tint="?attr/colorAccent" - tools:ignore="ContentDescription" /> + android:contentDescription="@string/notification_action_repeat" /> + android:contentDescription="@string/previous_stream" /> + android:tint="?attr/colorAccent" + android:contentDescription="@string/rewind" /> + android:contentDescription="@string/play" /> + android:tint="?attr/colorAccent" + android:contentDescription="@string/forward" /> + android:contentDescription="@string/next_stream" /> + android:contentDescription="@string/notification_action_shuffle" /> diff --git a/app/src/main/res/layout/dialog_feed_group_create.xml b/app/src/main/res/layout/dialog_feed_group_create.xml index 464940238..ca4f34f6a 100644 --- a/app/src/main/res/layout/dialog_feed_group_create.xml +++ b/app/src/main/res/layout/dialog_feed_group_create.xml @@ -28,7 +28,7 @@ android:scaleType="centerInside" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:ignore="ContentDescription" + android:contentDescription="@string/select_icon" tools:src="@drawable/ic_asterisk" />