mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-10-14 06:57:39 +00:00
Merge branch 'dev' into refactor
Conflicts: app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java app/src/main/java/org/schabi/newpipe/player/PlayerService.java app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java build.gradle
This commit is contained in:
@@ -1294,9 +1294,8 @@ 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, this@VideoDetailFragment)
|
||||
}
|
||||
PlayerHolder.setListener(this@VideoDetailFragment)
|
||||
PlayerHolder.tryBindIfNeeded(requireContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,6 +34,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
|
||||
import java.util.function.Consumer
|
||||
@@ -140,23 +141,23 @@ class PlayerService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
val p = player
|
||||
if (Intent.ACTION_MEDIA_BUTTON == intent.action && p?.playQueue == 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
|
||||
destroyPlayerAndStopService()
|
||||
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);
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (p != null) {
|
||||
val oldPlayerType = p.playerType
|
||||
p.handleIntent(intent)
|
||||
p.handleIntentPost(oldPlayerType)
|
||||
p.UIs().get(MediaSessionPlayerUi::class)
|
||||
?.handleMediaButtonIntent(intent)
|
||||
}
|
||||
val oldPlayerType = player?.playerType
|
||||
player?.handleIntent(intent)
|
||||
player?.handleIntentPost(oldPlayerType)
|
||||
player?.UIs()?.get(MediaSessionPlayerUi::class.java)
|
||||
?.handleMediaButtonIntent(intent)
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -172,9 +172,11 @@ object 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(s)
|
||||
if (playerService != null && playerService?.player != 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.service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
}
|
||||
|
||||
// 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
|
||||
/////////////////////////////////////////////////////
|
||||
|
@@ -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);
|
||||
|
@@ -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(" --> ");
|
||||
|
@@ -6,10 +6,9 @@ import static coil3.Image_androidKt.toBitmap;
|
||||
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.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
@@ -25,6 +24,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;
|
||||
|
||||
@@ -67,8 +67,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).
|
||||
* <p>
|
||||
* 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.
|
||||
@@ -82,44 +83,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 `<queries>` 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +178,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
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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) {
|
||||
//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) {
|
||||
@@ -448,7 +467,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;
|
||||
@@ -512,6 +531,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);
|
||||
@@ -584,8 +612,13 @@ public class DownloadManager {
|
||||
ArrayList<Mission> finished = new ArrayList<>(mMissionsFinished);
|
||||
List<Mission> 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 dm && canRecoverMission(dm)) {
|
||||
return false; // Don't remove recoverable missions
|
||||
}
|
||||
return pending.remove(mission) || finished.remove(mission);
|
||||
});
|
||||
|
||||
int fakeTotal = pending.size();
|
||||
if (fakeTotal > 0) fakeTotal++;
|
||||
|
@@ -16,7 +16,7 @@ desugar-jdk-libs-nio = "2.0.4"
|
||||
documentFile = "1.0.1"
|
||||
exoplayer = "2.18.7"
|
||||
fragment-compose = "1.8.2"
|
||||
gradle = "8.7.1"
|
||||
gradle = "8.13.0"
|
||||
groupie = "2.10.1"
|
||||
hilt = "2.51.1"
|
||||
jetpack-compose = "2024.10.01"
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
Reference in New Issue
Block a user