From 9282cce6a84d157615e2218f991ffed4efd7e522 Mon Sep 17 00:00:00 2001 From: whistlingwoods <72640314+whistlingwoods@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:41:03 +0530 Subject: [PATCH 1/9] fix: unfinished downloads disappear from the downloads list after app gets killed Author: InfinityLoop1308 Adapted for NewPipe from a fork's this commit https://github.com/InfinityLoop1308/PipePipeClient/commit/1cf059ce5e947aadb6dacd93b2ac9ca81b79cf46 --- .../us/shandian/giga/get/DownloadMission.java | 3 +- .../giga/service/DownloadManager.java | 60 +++++++++++++++---- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 04930b002..54340ce5d 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -661,7 +661,8 @@ public class DownloadMission extends Mission { * @return {@code true}, if storage is invalid and cannot be used */ public boolean hasInvalidStorage() { - return errCode == ERROR_PROGRESS_LOST || storage == null || !storage.existsAsFile(); + // Don't consider ERROR_PROGRESS_LOST as invalid storage - it can be recovered + return storage == null || !storage.existsAsFile(); } /** diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 9b90fa14b..6aff4daf2 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -24,6 +24,8 @@ import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; public class DownloadManager { private static final String TAG = DownloadManager.class.getSimpleName(); @@ -149,12 +151,31 @@ public class DownloadManager { if (sub.getName().equals(".tmp")) continue; DownloadMission mis = Utility.readFromFile(sub); - if (mis == null || mis.isFinished() || mis.hasInvalidStorage()) { + if (mis == null) { //noinspection ResultOfMethodCallIgnored sub.delete(); continue; } + // DON'T delete missions that are truly finished - let them be moved to finished list + if (mis.isFinished()) { + // Move to finished missions instead of deleting + setFinished(mis); + //noinspection ResultOfMethodCallIgnored + sub.delete(); + continue; + } + + // DON'T delete missions with storage issues - try to recover them + if (mis.hasInvalidStorage() && mis.errCode != ERROR_PROGRESS_LOST) { + // Only delete if it's truly unrecoverable (not just progress lost) + if (mis.storage == null && mis.errCode != ERROR_PROGRESS_LOST) { + //noinspection ResultOfMethodCallIgnored + sub.delete(); + continue; + } + } + mis.threads = new Thread[0]; boolean exists; @@ -163,16 +184,13 @@ public class DownloadManager { exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); } catch (Exception ex) { Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex); - mis.storage.invalidate(); + // Don't invalidate storage immediately - try to recover first exists = false; } if (mis.isPsRunning()) { if (mis.psAlgorithm.worksOnSameFile) { // Incomplete post-processing results in a corrupted download file - // because the selected algorithm works on the same file to save space. - // the file will be deleted if the storage API - // is Java IO (avoid showing the "Save as..." dialog) if (exists && mis.storage.isDirect() && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); } @@ -181,10 +199,11 @@ public class DownloadManager { mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; } else if (!exists) { tryRecover(mis); - - // the progress is lost, reset mission state - if (mis.isInitialized()) - mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST); + // Keep the mission even if recovery fails - don't reset to ERROR_PROGRESS_LOST + // This allows user to see the failed download and potentially retry + if (mis.isInitialized() && mis.errCode == ERROR_NOTHING) { + mis.resetState(true, true, ERROR_PROGRESS_LOST); + } } if (mis.psAlgorithm != null) { @@ -446,7 +465,7 @@ public class DownloadManager { continue; resumeMission(mission); - if (mission.errCode != DownloadMission.ERROR_NOTHING) continue; + if (mission.errCode != ERROR_NOTHING) continue; if (mPrefQueueLimit) return true; flag = true; @@ -510,6 +529,15 @@ public class DownloadManager { } } + public boolean canRecoverMission(DownloadMission mission) { + if (mission == null) return false; + + // Can recover missions with progress lost or storage issues + return mission.errCode == ERROR_PROGRESS_LOST || + mission.storage == null || + !mission.storage.existsAsFile(); + } + public MissionState checkForExistingMission(StoredFileHelper storage) { synchronized (this) { DownloadMission pending = getPendingMission(storage); @@ -582,8 +610,16 @@ public class DownloadManager { ArrayList finished = new ArrayList<>(mMissionsFinished); List remove = new ArrayList<>(hidden); - // hide missions (if required) - remove.removeIf(mission -> pending.remove(mission) || finished.remove(mission)); + // Don't hide recoverable missions + remove.removeIf(mission -> { + if (mission instanceof DownloadMission) { + DownloadMission dm = (DownloadMission) mission; + if (canRecoverMission(dm)) { + return false; // Don't remove recoverable missions + } + } + return pending.remove(mission) || finished.remove(mission); + }); int fakeTotal = pending.size(); if (fakeTotal > 0) fakeTotal++; From 83a0abddcc3c32dbacc2b206c1e5c20cc682204b Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Fri, 5 Sep 2025 17:19:32 +0200 Subject: [PATCH 2/9] Fix and simplify `openUrlInBrowser` The code was not previously working in case no default browser is set[1] AND NewPipe is set as default handler for the link in question. We improve it by telling the system to choose the target app as if the URI was `http://`, which works even if the user has not set a default browser. [1]: also the case if platform refuses to tell an app what the user's default browser is, which I observed on CalyxOS. --- .../external_communication/ShareUtils.java | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index 7524e5413..9fe351b4b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -5,10 +5,9 @@ import static org.schabi.newpipe.MainActivity.DEBUG; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; @@ -23,6 +22,7 @@ import androidx.core.content.FileProvider; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.PicassoHelper; @@ -62,8 +62,9 @@ public final class ShareUtils { } /** - * Open the url with the system default browser. If no browser is set as default, falls back to - * {@link #openAppChooser(Context, Intent, boolean)}. + * Open the url with the system default browser. If no browser is installed, falls back to + * {@link #openAppChooser(Context, Intent, boolean)} (for displaying that no apps are available + * to handle the action, or possible OEM-related edge cases). *

* This function selects the package to open based on which apps respond to the {@code http://} * schema alone, which should exclude special non-browser apps that are can handle the url (e.g. @@ -77,44 +78,26 @@ public final class ShareUtils { * @param url the url to browse **/ public static void openUrlInBrowser(@NonNull final Context context, final String url) { - // Resolve using a generic http://, so we are sure to get a browser and not e.g. the yt app. + // Target a generic http://, so we are sure to get a browser and not e.g. the yt app. // Note that this requires the `http` schema to be added to `` in the manifest. - final ResolveInfo defaultBrowserInfo; final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - defaultBrowserInfo = context.getPackageManager().resolveActivity(browserIntent, - PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY)); - } else { - defaultBrowserInfo = context.getPackageManager().resolveActivity(browserIntent, - PackageManager.MATCH_DEFAULT_ONLY); - } final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (defaultBrowserInfo == null) { - // No app installed to open a web URL, but it may be handled by other apps so try - // opening a system chooser for the link in this case (it could be bypassed by the - // system if there is only one app which can open the link or a default app associated - // with the link domain on Android 12 and higher) + // See https://stackoverflow.com/a/58801285 and `setSelector` documentation + intent.setSelector(browserIntent); + try { + context.startActivity(intent); + } catch (final ActivityNotFoundException e) { + // No browser is available. This should, in the end, yield a nice AOSP error message + // indicating that no app is available to handle this action. + // + // Note: there are some situations where modified OEM ROMs have apps that appear + // to be browsers but are actually app choosers. If starting the Activity fails + // related to this, opening the system app chooser is still the correct behavior. + intent.setSelector(null); openAppChooser(context, intent, true); - return; - } - - final String defaultBrowserPackage = defaultBrowserInfo.activityInfo.packageName; - - if (defaultBrowserPackage.equals("android")) { - // No browser set as default (doesn't work on some devices) - openAppChooser(context, intent, true); - } else { - try { - intent.setPackage(defaultBrowserPackage); - context.startActivity(intent); - } catch (final ActivityNotFoundException e) { - // Not a browser but an app chooser because of OEMs changes - intent.setPackage(null); - openAppChooser(context, intent, true); - } } } @@ -190,6 +173,18 @@ public final class ShareUtils { chooserIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.open_with)); } + // Avoid opening in NewPipe + // (Implementation note: if the URL is one for which NewPipe itself + // is set as handler on Android >= 12, we actually remove the only eligible app + // for this link, and browsers will not be offered to the user. For that, use + // `openUrlInBrowser`.) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + chooserIntent.putExtra( + Intent.EXTRA_EXCLUDE_COMPONENTS, + new ComponentName[]{new ComponentName(context, RouterActivity.class)} + ); + } + // Migrate any clip data and flags from the original intent. final int permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION From 21e24c9e34c7b0fb5aaee0e0eb7c36d985f76808 Mon Sep 17 00:00:00 2001 From: whistlingwoods <72640314+whistlingwoods@users.noreply.github.com> Date: Sat, 6 Sep 2025 19:14:15 +0530 Subject: [PATCH 3/9] Apply review suggestions --- .../java/us/shandian/giga/service/DownloadManager.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 6aff4daf2..8d841a207 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -169,7 +169,7 @@ public class DownloadManager { // DON'T delete missions with storage issues - try to recover them if (mis.hasInvalidStorage() && mis.errCode != ERROR_PROGRESS_LOST) { // Only delete if it's truly unrecoverable (not just progress lost) - if (mis.storage == null && mis.errCode != ERROR_PROGRESS_LOST) { + if (mis.storage == null) { //noinspection ResultOfMethodCallIgnored sub.delete(); continue; @@ -612,11 +612,8 @@ public class DownloadManager { // Don't hide recoverable missions remove.removeIf(mission -> { - if (mission instanceof DownloadMission) { - DownloadMission dm = (DownloadMission) mission; - if (canRecoverMission(dm)) { - return false; // Don't remove recoverable missions - } + if (mission instanceof DownloadMission dm && canRecoverMission(dm)) { + return false; // Don't remove recoverable missions } return pending.remove(mission) || finished.remove(mission); }); From 2b7c72eb694835906ac555b4c9a474e54675e63e Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 8 Sep 2025 08:03:57 +0530 Subject: [PATCH 4/9] Update AGP to 8.13.0 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d93abc4c0..3cdc0dc59 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.7.1' + classpath 'com.android.tools.build:gradle:8.13.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4ea536e77..57939822b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 92a07a34456c5df560454a335f89ba677719c313 Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 5 Sep 2025 22:47:45 +0200 Subject: [PATCH 5/9] Use tryBindIfNeeded(), send player started only if player!=null This commit fixes one way ghost notifications could be produced (although I don't know if there are other ways). This is the call chain that would lead to ghost notifications being created: 1. the system starts `PlayerService` to query information from it, without providing `SHOULD_START_FOREGROUND_EXTRA=true`, so NewPipe does not start the player nor show any notification, as expected 2. the `PlayerHolder::serviceConnection.onServiceConnected()` gets called by the system to inform `PlayerHolder` that the player started 3. `PlayerHolder` notifies `MainActivity` that the player has started (although in fact only the service has started), by sending a `ACTION_PLAYER_STARTED` broadcast 4. `MainActivity` receives the `ACTION_PLAYER_STARTED` broadcast and brings up the mini-player, but then also tries to make `PlayerHolder` bind to `PlayerService` just in case it was not bound yet, but does so using `PlayerHolder::startService()` instead of the more passive `PlayerHolder::tryBindIfNeeded()` 5. `PlayerHolder::startService()` sends an intent to the `PlayerService` again, this time with `startForegroundService` and with `SHOULD_START_FOREGROUND_EXTRA=true` 6. the `PlayerService` receives the intent and due to `SHOULD_START_FOREGROUND_EXTRA=true` decides to start up the player and show a dummy notification Steps 3 and 4 are wrong, and this commit fixes them: 3. `PlayerHolder` will now broadcast `ACTION_PLAYER_STARTED` when the service connects, only if the player is not-null 4. `PlayerHolder::tryBindIfNeeded()` is now used to passively try to bind, instead of `PlayerHolder::startService()` --- .../newpipe/fragments/detail/VideoDetailFragment.java | 6 ++---- .../org/schabi/newpipe/player/helper/PlayerHolder.java | 8 +++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c43007da4..7b8705565 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1416,10 +1416,8 @@ public final class VideoDetailFragment bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } // Rebound to the service if it was closed via notification or mini player - if (!playerHolder.isBound()) { - playerHolder.startService( - false, VideoDetailFragment.this); - } + playerHolder.setListener(VideoDetailFragment.this); + playerHolder.tryBindIfNeeded(context); break; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 97f2d6717..9edfc804a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -192,9 +192,11 @@ public final class PlayerHolder { startPlayerListener(); // ^ will call listener.onPlayerConnected() down the line if there is an active player - // notify the main activity that binding the service has completed, so that it can - // open the bottom mini-player - NavigationHelper.sendPlayerStartedEvent(localBinder.getService()); + if (playerService != null && playerService.getPlayer() != null) { + // notify the main activity that binding the service has completed and that there is + // a player, so that it can open the bottom mini-player + NavigationHelper.sendPlayerStartedEvent(localBinder.getService()); + } } } From aa2b4821e26e8432c04a0fa5afc7a6eb7145ce7c Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 17 Sep 2025 11:45:02 +0200 Subject: [PATCH 6/9] Post dummy notification then close player service on invalid intent This should solve "Context.startForegroundService() did not then call Service.startForeground()" according to https://github.com/TeamNewPipe/NewPipe/issues/12489#issuecomment-3290318112 --- .../schabi/newpipe/player/PlayerService.java | 30 ++++----- .../player/notification/NotificationUtil.java | 61 ++++++++++++------- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java index 3b6224b47..dba30f9e8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -40,6 +40,7 @@ import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl; import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi; +import org.schabi.newpipe.player.notification.NotificationUtil; import org.schabi.newpipe.util.ThemeHelper; import java.lang.ref.WeakReference; @@ -156,25 +157,24 @@ public final class PlayerService extends MediaBrowserServiceCompat { } } - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - && (player == null || player.getPlayQueue() == null)) { - /* - No need to process media button's actions if the player is not working, otherwise - the player service would strangely start with nothing to play - Stop the service in this case, which will be removed from the foreground and its - notification cancelled in its destruction - */ + if (player == null) { + // No need to process media button's actions or other system intents if the player is + // not running. However, since the current intent might have been issued by the system + // with `startForegroundService()` (for unknown reasons), we need to ensure that we post + // a (dummy) foreground notification, otherwise we'd incur in + // "Context.startForegroundService() did not then call Service.startForeground()". Then + // we stop the service again. + Log.d(TAG, "onStartCommand() got a useless intent, closing the service"); + NotificationUtil.startForegroundWithDummyNotification(this); destroyPlayerAndStopService(); return START_NOT_STICKY; } - if (player != null) { - final PlayerType oldPlayerType = player.getPlayerType(); - player.handleIntent(intent); - player.handleIntentPost(oldPlayerType); - player.UIs().get(MediaSessionPlayerUi.class) - .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); - } + final PlayerType oldPlayerType = player.getPlayerType(); + player.handleIntent(intent); + player.handleIntentPost(oldPlayerType); + player.UIs().get(MediaSessionPlayerUi.class) + .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); return START_NOT_STICKY; } diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java index 79ae81de2..9b9c47b0e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java @@ -5,7 +5,9 @@ import static androidx.media.app.NotificationCompat.MediaStyle; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE; import android.annotation.SuppressLint; +import android.app.Notification; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.content.pm.ServiceInfo; import android.graphics.Bitmap; @@ -24,6 +26,7 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.PlayerIntentType; +import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.util.NavigationHelper; @@ -90,12 +93,9 @@ public final class NotificationUtil { Log.d(TAG, "createNotification()"); } notificationManager = NotificationManagerCompat.from(player.getContext()); - final NotificationCompat.Builder builder = - new NotificationCompat.Builder(player.getContext(), - player.getContext().getString(R.string.notification_channel_id)); - final MediaStyle mediaStyle = new MediaStyle(); // setup media style (compact notification slots and media session) + final MediaStyle mediaStyle = new MediaStyle(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { // notification actions are ignored on Android 13+, and are replaced by code in // MediaSessionPlayerUi @@ -108,18 +108,9 @@ public final class NotificationUtil { .ifPresent(mediaStyle::setMediaSession); // setup notification builder - builder.setStyle(mediaStyle) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setCategory(NotificationCompat.CATEGORY_TRANSPORT) - .setShowWhen(false) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setColor(ContextCompat.getColor(player.getContext(), - R.color.dark_background_color)) + final var builder = setupNotificationBuilder(player.getContext(), mediaStyle) .setColorized(player.getPrefs().getBoolean( - player.getContext().getString(R.string.notification_colorize_key), true)) - .setDeleteIntent(PendingIntentCompat.getBroadcast(player.getContext(), - NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT, false)); + player.getContext().getString(R.string.notification_colorize_key), true)); // set the initial value for the video thumbnail, updatable with updateNotificationThumbnail setLargeIcon(builder); @@ -168,17 +159,17 @@ public final class NotificationUtil { && notificationBuilder.mActions.get(2).actionIntent != null); } + public static void startForegroundWithDummyNotification(final PlayerService service) { + final var builder = setupNotificationBuilder(service, new MediaStyle()); + startForeground(service, builder.build()); + } + public void createNotificationAndStartForeground() { if (notificationBuilder == null) { notificationBuilder = createNotification(); } updateNotification(); - - // ServiceInfo constants are not used below Android Q, so 0 is set here - final int serviceType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - ? ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK : 0; - ServiceCompat.startForeground(player.getService(), NOTIFICATION_ID, - notificationBuilder.build(), serviceType); + startForeground(player.getService(), notificationBuilder.build()); } public void cancelNotificationAndStopForeground() { @@ -192,6 +183,34 @@ public final class NotificationUtil { } + ///////////////////////////////////////////////////// + // STATIC FUNCTIONS IN COMMON BETWEEN DUMMY AND REAL NOTIFICATION + ///////////////////////////////////////////////////// + + private static NotificationCompat.Builder setupNotificationBuilder(final Context context, + final MediaStyle style) { + return new NotificationCompat.Builder(context, + context.getString(R.string.notification_channel_id)) + .setStyle(style) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_TRANSPORT) + .setShowWhen(false) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setColor(ContextCompat.getColor(context, R.color.dark_background_color)) + .setDeleteIntent(PendingIntentCompat.getBroadcast(context, + NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT, false)); + } + + private static void startForeground(final PlayerService service, + final Notification notification) { + // ServiceInfo constants are not used below Android Q, so 0 is set here + final int serviceType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK : 0; + ServiceCompat.startForeground(service, NOTIFICATION_ID, notification, serviceType); + } + + ///////////////////////////////////////////////////// // ACTIONS ///////////////////////////////////////////////////// From 4e9a480fdd4beaaa17a65e00bff258b8a26c483d Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 17 Sep 2025 12:23:37 +0200 Subject: [PATCH 7/9] Enforce using SAF on FireOS TVs with Android 10+ Even if SAF is bugged there, there is no other way to open a file dialog, since NewPipe does not have permissions, see #10643 --- .../org/schabi/newpipe/settings/NewPipeSettings.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 0a5512c69..7cb1564b3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -103,12 +103,12 @@ public final class NewPipeSettings { } public static boolean useStorageAccessFramework(final Context context) { - // There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with a - // remote (see #6455). - if (DeviceUtils.isFireTv()) { - return false; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return true; + } else if (DeviceUtils.isFireTv()) { + // There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with + // a remote (see #6455). + return false; } final String key = context.getString(R.string.storage_use_saf); From 980e8f3951c4f166f1596639502a995a574dd01a Mon Sep 17 00:00:00 2001 From: TransZAllen Date: Mon, 29 Sep 2025 14:04:46 +0800 Subject: [PATCH 8/9] [YouTube] *.srt numbering start at 1 instead of 0. (issue: https://github.com/TeamNewPipe/NewPipe/issues/12670) - The SubRip (.srt) specification requires subtitle numbering to begin from 1. - Please refer to https://en.wikipedia.org/wiki/SubRip - Previously numbering started from 0, which is accepted by most players (tested on mpv, VLC, MPlayer, Totem) but not strictly compliant. --- .../org/schabi/newpipe/streams/SrtFromTtmlWriter.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java index 7aff655a0..1ec78ba21 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java @@ -24,7 +24,11 @@ public class SrtFromTtmlWriter { private final boolean ignoreEmptyFrames; private final Charset charset = StandardCharsets.UTF_8; - private int frameIndex = 0; + // According to the SubRip (.srt) specification, subtitle + // numbering must start from 1. + // Some players accept 0 or even negative indices, + // but to ensure compliance we start at 1. + private int frameIndex = 1; public SrtFromTtmlWriter(final SharpStream out, final boolean ignoreEmptyFrames) { this.out = out; @@ -39,7 +43,8 @@ public class SrtFromTtmlWriter { private void writeFrame(final String begin, final String end, final StringBuilder text) throws IOException { - writeString(String.valueOf(frameIndex++)); + writeString(String.valueOf(frameIndex)); + frameIndex += 1; writeString(NEW_LINE); writeString(begin); writeString(" --> "); From 59dfdda95e46aad3c42db401885b136a8ff33b8b Mon Sep 17 00:00:00 2001 From: Thonsi <10291398+thonsi@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:56:08 +0000 Subject: [PATCH 9/9] remove isUsingDSP --- .../java/org/schabi/newpipe/player/helper/AudioReactor.java | 3 --- .../java/org/schabi/newpipe/player/helper/PlayerHelper.java | 4 ---- 2 files changed, 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index a05990816..084336d54 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -154,9 +154,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An notifyAudioSessionUpdate(true, audioSessionId); } private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) { - if (!PlayerHelper.isUsingDSP()) { - return; - } final Intent intent = new Intent(active ? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION : AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 3c69ff78b..c335e9b7c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -296,10 +296,6 @@ public final class PlayerHelper { AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION); } - public static boolean isUsingDSP() { - return true; - } - @NonNull public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { final CaptioningManager captioningManager = ContextCompat.getSystemService(context,