Improve accessibility for video lists and player details

This commit is contained in:
aryan 2025-12-27 11:20:46 +05:30 committed by tobigr
parent d859a5edc8
commit c29ae0e885
8 changed files with 245 additions and 14 deletions

View File

@ -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);
}
}

View File

@ -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" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_backward"
@ -191,7 +191,7 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_previous"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/previous_stream" />
<ImageButton
android:id="@+id/control_fast_rewind"
@ -205,7 +205,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_rewind"
android:tint="?attr/colorAccent" />
android:tint="?attr/colorAccent"
android:contentDescription="@string/rewind" />
<ImageButton
android:id="@+id/control_play_pause"
@ -221,7 +222,7 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_pause"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/play" />
<ProgressBar
android:id="@+id/control_progress_bar"
@ -255,7 +256,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_fastforward"
android:tint="?attr/colorAccent" />
android:tint="?attr/colorAccent"
android:contentDescription="@string/forward" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward"
@ -270,7 +272,7 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_next"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/next_stream" />
<ImageButton
android:id="@+id/control_shuffle"
@ -286,7 +288,7 @@
android:scaleType="fitXY"
android:src="@drawable/ic_shuffle"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/notification_action_shuffle" />
</RelativeLayout>
</RelativeLayout>

View File

@ -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" />
<com.google.android.material.textfield.TextInputLayout
@ -195,7 +195,7 @@
android:scaleType="centerInside"
android:src="@drawable/ic_delete"
android:visibility="gone"
tools:ignore="ContentDescription"
android:contentDescription="@string/delete"
tools:visibility="visible" />
<Button

View File

@ -56,8 +56,8 @@
android:layout_gravity="center"
android:background="@android:color/transparent"
android:src="@drawable/ic_play_arrow_shadow"
android:contentDescription="@string/play"
android:visibility="invisible"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<org.schabi.newpipe.views.NewPipeTextView
@ -188,7 +188,7 @@
android:layout_marginTop="11dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_expand_more"
tools:ignore="ContentDescription" />
android:contentDescription="@string/show_more" />
</FrameLayout>
@ -614,7 +614,7 @@
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:scaleType="fitCenter"
tools:ignore="ContentDescription" />
android:contentDescription="@string/switch_to_main" />
<LinearLayout
android:id="@+id/overlay_metadata_layout"

View File

@ -15,6 +15,7 @@
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:src="@drawable/placeholder_thumbnail_video"
app:layout_constraintBottom_toTopOf="@+id/itemProgressView"
app:layout_constraintStart_toStartOf="parent"

View File

@ -18,6 +18,7 @@
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:src="@drawable/placeholder_thumbnail_video"
tools:ignore="RtlHardcoded" />

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="accessibility_action_show_options" type="id"/>
<item name="accessibility_action_enqueue" type="id"/>
<item name="accessibility_action_enqueue_next" type="id"/>
<item name="accessibility_action_background" type="id"/>
<item name="accessibility_action_popup" type="id"/>
<item name="accessibility_action_download" type="id"/>
<item name="accessibility_action_playlist" type="id"/>
<item name="accessibility_action_share" type="id"/>
<item name="accessibility_action_browser" type="id"/>
<item name="accessibility_action_kodi" type="id"/>
<item name="accessibility_action_mark_watched" type="id"/>
<item name="accessibility_action_channel_details" type="id"/>
</resources>

View File

@ -576,6 +576,7 @@
<string name="minimize_on_exit_none_description">None</string>
<string name="minimize_on_exit_background_description">Minimize to background player</string>
<string name="minimize_on_exit_popup_description">Minimize to popup player</string>
<string name="select_icon">Select icon</string>
<!-- Autoplay behavior -->
<string name="autoplay_summary">Start playback automatically — %s</string>
<string name="wifi_only">Only on Wi-Fi</string>