Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,44 +45,43 @@
* </p>
*/
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()");
}),
Expand All @@ -90,62 +90,63 @@ 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"
)
)
),

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");
}
Comment on lines +120 to 129
Copy link
Contributor

@TobiGr TobiGr Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Stypox I am not quite sure about the change I made here. Is the check even necessary anymore because we are now using the activity? Should fragementActivity.isDestroyed() or fragementActivity.isFinishing() be used instead? The check was initially introduced to ensure that opening the dialog would not lead to a crash when the fragment was closed while loading the fetching the required info, e.g. by going back.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TobiGr Thanks for the review and the cleanup! I appreciate you handling the deduplication!

})
),

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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}

Loading