diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java index cbaae2834b8..a8e7401e7e4 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.preference.PreferenceManager; import org.schabi.newpipe.App; @@ -73,7 +74,7 @@ private InfoItemDialog(@NonNull final Activity activity, // Call an entry's action / onClick method when the entry is selected. final DialogInterface.OnClickListener action = (d, index) -> - entries.get(index).action.onClick(fragment, info); + entries.get(index).action.onClick((FragmentActivity) activity, info); dialog = new AlertDialog.Builder(activity) .setCustomTitle(bannerView) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java index 5676fee95c1..017dd2cb183 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import androidx.fragment.app.FragmentManager; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -44,44 +45,43 @@ *
*/ public enum StreamDialogDefaultEntry { - SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> { - final var activity = fragment.requireActivity(); - fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(), - item.getUploaderUrl(), url -> openChannelFragment(activity, item, url)); - }), + SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragmentActivity, item) -> + fetchUploaderUrlIfSparse(fragmentActivity, item.getServiceId(), item.getUrl(), + item.getUploaderUrl(), url -> openChannelFragment(fragmentActivity, item, url)) + ), /** * Enqueues the stream automatically to the current PlayerType. */ - ENQUEUE(R.string.enqueue_stream, (fragment, item) -> - fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> - NavigationHelper.enqueueOnPlayer(fragment.getContext(), singlePlayQueue)) + ENQUEUE(R.string.enqueue_stream, (fragmentActivity, item) -> + fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue -> + NavigationHelper.enqueueOnPlayer(fragmentActivity, singlePlayQueue)) ), /** * Enqueues the stream automatically to the current PlayerType * after the currently playing stream. */ - ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragment, item) -> - fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> - NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), singlePlayQueue)) + ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragmentActivity, item) -> + fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue -> + NavigationHelper.enqueueNextOnPlayer(fragmentActivity, singlePlayQueue)) ), - START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) -> - fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragmentActivity, item) -> + fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue -> NavigationHelper.playOnBackgroundPlayer( - fragment.getContext(), singlePlayQueue, true))), + fragmentActivity, singlePlayQueue, true))), - START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragment, item) -> - fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> - NavigationHelper.playOnPopupPlayer(fragment.getContext(), singlePlayQueue, true))), + START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragmentActivity, item) -> + fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue -> + NavigationHelper.playOnPopupPlayer(fragmentActivity, singlePlayQueue, true))), - SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> { + SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragmentActivity, item) -> { throw new UnsupportedOperationException("This needs to be implemented manually " + "by using InfoItemDialog.Builder.setAction()"); }), - DELETE(R.string.delete, (fragment, item) -> { + DELETE(R.string.delete, (fragmentActivity, item) -> { throw new UnsupportedOperationException("This needs to be implemented manually " + "by using InfoItemDialog.Builder.setAction()"); }), @@ -90,12 +90,12 @@ public enum StreamDialogDefaultEntry { * Opens a {@link PlaylistDialog} to either append the stream to a playlist * or create a new playlist if there are no local playlists. */ - APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) -> + APPEND_PLAYLIST(R.string.add_to_playlist, (fragmentActivity, item) -> PlaylistDialog.createCorrespondingDialog( - fragment.getContext(), + fragmentActivity, List.of(new StreamEntity(item)), dialog -> dialog.show( - fragment.getParentFragmentManager(), + fragmentActivity.getSupportFragmentManager(), "StreamDialogEntry@" + (dialog instanceof PlaylistAppendDialog ? "append" : "create") + "_playlist" @@ -103,49 +103,50 @@ public enum StreamDialogDefaultEntry { ) ), - PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> - KoreUtils.playWithKore(fragment.requireContext(), Uri.parse(item.getUrl()))), + PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragmentActivity, item) -> + KoreUtils.playWithKore(fragmentActivity, Uri.parse(item.getUrl()))), - SHARE(R.string.share, (fragment, item) -> - ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), + SHARE(R.string.share, (fragmentActivity, item) -> + ShareUtils.shareText(fragmentActivity, item.getName(), item.getUrl(), item.getThumbnails())), /** * Opens a {@link DownloadDialog} after fetching some stream info. - * If the user quits the current fragment, it will not open a DownloadDialog. + * If the user quits the current fragmentActivity, it will not open a DownloadDialog. */ - DOWNLOAD(R.string.download, (fragment, item) -> - fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(), + DOWNLOAD(R.string.download, (fragmentActivity, item) -> + fetchStreamInfoAndSaveToDatabase(fragmentActivity, item.getServiceId(), item.getUrl(), info -> { - // Ensure the fragment is attached and its state hasn't been saved to avoid + // Ensure the fragment in the activity is attached + // and its state hasn't been saved to avoid // showing dialog during lifecycle changes or when the activity is paused, // e.g. by selecting the download option and opening a different fragment. - if (fragment.isAdded() && !fragment.isStateSaved()) { + final FragmentManager fm = fragmentActivity.getSupportFragmentManager(); + if (!fm.isStateSaved()) { final DownloadDialog downloadDialog = - new DownloadDialog(fragment.requireContext(), info); - downloadDialog.show(fragment.getChildFragmentManager(), - "downloadDialog"); + new DownloadDialog(fragmentActivity, info); + downloadDialog.show(fm, "downloadDialog"); } }) ), - OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) -> - ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), + OPEN_IN_BROWSER(R.string.open_in_browser, (fragmentActivity, item) -> + ShareUtils.openUrlInBrowser(fragmentActivity, item.getUrl())), - MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) -> - new HistoryRecordManager(fragment.getContext()) + MARK_AS_WATCHED(R.string.mark_as_watched, (fragmentActivity, item) -> + new HistoryRecordManager(fragmentActivity) .markAsWatched(item) - .doOnError(error -> { + .doOnError(error -> ErrorUtil.showSnackbar( - fragment.requireContext(), + fragmentActivity, new ErrorInfo( error, UserAction.OPEN_INFO_ITEM_DIALOG, "Got an error when trying to mark as watched" ) - ); - }) + ) + ) .onErrorComplete() .observeOn(AndroidSchedulers.mainThread()) .subscribe() diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java index 9d82e3b5829..8cba26bb47b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java @@ -4,7 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -26,6 +26,6 @@ public String getString(@NonNull final Context context) { } public interface StreamDialogEntryAction { - void onClick(Fragment fragment, StreamInfoItem infoItem); + void onClick(FragmentActivity fragmentActivity, StreamInfoItem infoItem); } } 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 642738630c3..8387367970f 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,22 +1,34 @@ package org.schabi.newpipe.info_list.holder; +import android.content.Context; +import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +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.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.ktx.ViewUtils; 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.StreamTypeUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.image.CoilHelper; import org.schabi.newpipe.views.AnimatedProgressBar; @@ -57,7 +69,8 @@ public void updateFromItem(final InfoItem infoItem, 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 +89,8 @@ public void updateFromItem(final InfoItem infoItem, } } 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 +159,142 @@ private void enableLongClick(final StreamInfoItem item) { } return true; }); + + updateAccessibilityActions(item); + } + + private void updateAccessibilityActions(final StreamInfoItem item) { + ViewCompat.setAccessibilityDelegate(itemView, new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo( + @NonNull final View host, + @NonNull 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.accessibility_show_channel_details, + item.getUploaderName()))); + } + + info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.accessibility_action_show_options, + context.getString(R.string.more_options))); + } + + @Override + public boolean performAccessibilityAction(@NonNull final View host, final int action, + final Bundle args) { + final Context context = itemBuilder.getContext(); + final FragmentActivity fActivity = ((FragmentActivity) context); + + if (context == null) { + return super.performAccessibilityAction(host, action, args); + } + + if (action == R.id.accessibility_action_show_options) { + // display stream dialog + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().held(item); + } + } else if (action == R.id.accessibility_action_enqueue) { + StreamDialogDefaultEntry.ENQUEUE.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_enqueue_next) { + StreamDialogDefaultEntry.ENQUEUE_NEXT.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_background) { + StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND.action.onClick( + fActivity, item); + } else if (action == R.id.accessibility_action_popup) { + StreamDialogDefaultEntry.START_HERE_ON_POPUP.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_download) { + StreamDialogDefaultEntry.DOWNLOAD.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_playlist) { + StreamDialogDefaultEntry.APPEND_PLAYLIST.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_share) { + StreamDialogDefaultEntry.SHARE.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_browser) { + StreamDialogDefaultEntry.OPEN_IN_BROWSER.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_kodi) { + StreamDialogDefaultEntry.PLAY_WITH_KODI.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_mark_watched) { + StreamDialogDefaultEntry.MARK_AS_WATCHED.action.onClick(fActivity, item); + } else if (action == R.id.accessibility_action_channel_details) { + StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS.action.onClick(fActivity, item); + } else { + return super.performAccessibilityAction(host, action, args); + } + // return true if the action was handled + return true; + } + }); } 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 29efa36f92e..d2961bf5f89 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" />