mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-11-20 00:56:35 +01:00
code cleanup
* migrate few annotations to androidx * mission recovery: better error handling (except StreamExtractor.getErrorMessage() method always returns an error) * post-processing: more detailed progress [file specific changes] DownloadMission.java * remove redundant/boilerplate code (again) * make few variables volatile * better file "length" approximation * use "done" variable to count the amount of bytes downloaded (simplify percent calc in UI code) Postprocessing.java * if case of error use "ERROR_POSTPROCESSING" instead of "ERROR_UNKNOWN_EXCEPTION" * simplify source stream init DownloadManager.java * move all "service message sending" code to DownloadMission * remove not implemented method "notifyUserPendingDownloads()" also his unused strings DownloadManagerService.java * use START_STICKY instead of START_NOT_STICKY * simplify addMissionEventListener()/removeMissionEventListener() methods (always are called from the main thread) Deleter.java * better method definition MissionAdapter.java * better method definition * code cleanup * the UI is now refreshed every 750ms * simplify download progress calculation * indicates if the download is actually recovering * smooth download speed measure * show estimated remain time MainFragment.java: * check if viewPager is null (issued by "Apply changes" feature of Android Studio)
This commit is contained in:
parent
844f80a5f1
commit
f62a7919a5
@ -2,6 +2,15 @@ package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||
|
@ -1,9 +1,10 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -177,7 +178,7 @@ public class DownloadInitializer extends Thread {
|
||||
if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired
|
||||
interrupt();
|
||||
mMission.doRecover(e);
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4,18 +4,21 @@ import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.ConnectException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
@ -27,7 +30,7 @@ import us.shandian.giga.util.Utility;
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadMission extends Mission {
|
||||
private static final long serialVersionUID = 6L;// last bump: 28 september 2019
|
||||
private static final long serialVersionUID = 6L;// last bump: 07 october 2019
|
||||
|
||||
static final int BUFFER_SIZE = 64 * 1024;
|
||||
static final int BLOCK_SIZE = 512 * 1024;
|
||||
@ -61,9 +64,9 @@ public class DownloadMission extends Mission {
|
||||
public String[] urls;
|
||||
|
||||
/**
|
||||
* Number of bytes downloaded
|
||||
* Number of bytes downloaded and written
|
||||
*/
|
||||
public long done;
|
||||
public volatile long done;
|
||||
|
||||
/**
|
||||
* Indicates a file generated dynamically on the web server
|
||||
@ -119,7 +122,7 @@ public class DownloadMission extends Mission {
|
||||
/**
|
||||
* Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback}
|
||||
*/
|
||||
long fallbackResumeOffset;
|
||||
volatile long fallbackResumeOffset;
|
||||
|
||||
/**
|
||||
* Maximum of download threads running, chosen by the user
|
||||
@ -132,22 +135,23 @@ public class DownloadMission extends Mission {
|
||||
public MissionRecoveryInfo[] recoveryInfo;
|
||||
|
||||
private transient int finishCount;
|
||||
public transient boolean running;
|
||||
public transient volatile boolean running;
|
||||
public boolean enqueued;
|
||||
|
||||
public int errCode = ERROR_NOTHING;
|
||||
public Exception errObject = null;
|
||||
|
||||
public transient Handler mHandler;
|
||||
private transient boolean mWritingToFile;
|
||||
private transient boolean[] blockAcquired;
|
||||
|
||||
private transient long writingToFileNext;
|
||||
private transient volatile boolean writingToFile;
|
||||
|
||||
final Object LOCK = new Lock();
|
||||
|
||||
private transient boolean deleted;
|
||||
|
||||
public transient volatile Thread[] threads = new Thread[0];
|
||||
private transient Thread init = null;
|
||||
@NonNull
|
||||
public transient Thread[] threads = new Thread[0];
|
||||
public transient Thread init = null;
|
||||
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||
if (urls == null) throw new NullPointerException("urls is null");
|
||||
@ -246,8 +250,10 @@ public class DownloadMission extends Mission {
|
||||
int statusCode = conn.getResponseCode();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, threadId + ":Range=" + conn.getRequestProperty("Range"));
|
||||
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode);
|
||||
Log.d(TAG, threadId + ":[request] Range=" + conn.getRequestProperty("Range"));
|
||||
Log.d(TAG, threadId + ":[response] Code=" + statusCode);
|
||||
Log.d(TAG, threadId + ":[response] Content-Length=" + conn.getContentLength());
|
||||
Log.d(TAG, threadId + ":[response] Content-Range=" + conn.getHeaderField("Content-Range"));
|
||||
}
|
||||
|
||||
|
||||
@ -272,24 +278,19 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
synchronized void notifyProgress(long deltaLen) {
|
||||
if (!running) return;
|
||||
|
||||
if (unknownLength) {
|
||||
length += deltaLen;// Update length before proceeding
|
||||
}
|
||||
|
||||
done += deltaLen;
|
||||
|
||||
if (done > length) {
|
||||
done = length;
|
||||
}
|
||||
if (metadata == null) return;
|
||||
|
||||
if (done != length && !deleted && !mWritingToFile) {
|
||||
mWritingToFile = true;
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) {
|
||||
writingToFile = true;
|
||||
writingToFileNext = done + BLOCK_SIZE;
|
||||
writeThisToFileAsync();
|
||||
}
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
synchronized void notifyError(Exception err) {
|
||||
@ -342,43 +343,42 @@ public class DownloadMission extends Mission {
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_ERROR);
|
||||
|
||||
if (running) {
|
||||
running = false;
|
||||
if (threads != null) selfPause();
|
||||
}
|
||||
if (running) pauseThreads();
|
||||
}
|
||||
|
||||
synchronized void notifyFinished() {
|
||||
if (errCode > ERROR_NOTHING) return;
|
||||
|
||||
finishCount++;
|
||||
|
||||
if (blocks.length < 1 || threads == null || finishCount == threads.length) {
|
||||
if (errCode != ERROR_NOTHING) return;
|
||||
if (current < urls.length) {
|
||||
if (++finishCount < threads.length) return;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length);
|
||||
}
|
||||
|
||||
if ((current + 1) < urls.length) {
|
||||
// prepare next sub-mission
|
||||
long current_offset = offsets[current++];
|
||||
offsets[current] = current_offset + length;
|
||||
initializer();
|
||||
return;
|
||||
Log.d(TAG, "onFinish: downloaded " + (current + 1) + "/" + urls.length);
|
||||
}
|
||||
|
||||
current++;
|
||||
unknownLength = false;
|
||||
|
||||
if (!doPostprocessing()) return;
|
||||
|
||||
enqueued = false;
|
||||
running = false;
|
||||
deleteThisFromFile();
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||
if (current < urls.length) {
|
||||
// prepare next sub-mission
|
||||
offsets[current] = offsets[current - 1] + length;
|
||||
initializer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (psAlgorithm != null && psState == 0) {
|
||||
threads = new Thread[]{
|
||||
runAsync(1, this::doPostprocessing)
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// this mission is fully finished
|
||||
|
||||
unknownLength = false;
|
||||
enqueued = false;
|
||||
running = false;
|
||||
|
||||
deleteThisFromFile();
|
||||
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||
}
|
||||
|
||||
private void notifyPostProcessing(int state) {
|
||||
@ -396,10 +396,15 @@ public class DownloadMission extends Mission {
|
||||
|
||||
Log.d(TAG, action + " postprocessing on " + storage.getName());
|
||||
|
||||
if (state == 2) {
|
||||
psState = state;
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
// don't return without fully write the current state
|
||||
psState = state;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
writeThisToFile();
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,12 +416,7 @@ public class DownloadMission extends Mission {
|
||||
if (running || isFinished() || urls.length < 1) return;
|
||||
|
||||
// ensure that the previous state is completely paused.
|
||||
int maxWait = 10000;// 10 seconds
|
||||
joinForThread(init, maxWait);
|
||||
if (threads != null) {
|
||||
for (Thread thread : threads) joinForThread(thread, maxWait);
|
||||
threads = null;
|
||||
}
|
||||
joinForThreads(10000);
|
||||
|
||||
running = true;
|
||||
errCode = ERROR_NOTHING;
|
||||
@ -427,12 +427,14 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
if (current >= urls.length) {
|
||||
runAsync(1, this::notifyFinished);
|
||||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_RUNNING);
|
||||
|
||||
if (urls[current] == null) {
|
||||
doRecover(null);
|
||||
doRecover(ERROR_RESOURCE_GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -446,18 +448,13 @@ public class DownloadMission extends Mission {
|
||||
blockAcquired = new boolean[blocks.length];
|
||||
|
||||
if (blocks.length < 1) {
|
||||
if (unknownLength) {
|
||||
done = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))};
|
||||
} else {
|
||||
int remainingBlocks = 0;
|
||||
for (int block : blocks) if (block >= 0) remainingBlocks++;
|
||||
|
||||
if (remainingBlocks < 1) {
|
||||
runAsync(1, this::notifyFinished);
|
||||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -483,6 +480,7 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
running = false;
|
||||
notify(DownloadManagerService.MESSAGE_PAUSED);
|
||||
|
||||
if (init != null && init.isAlive()) {
|
||||
// NOTE: if start() method is running ¡will no have effect!
|
||||
@ -497,29 +495,14 @@ public class DownloadMission extends Mission {
|
||||
Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server).");
|
||||
}
|
||||
|
||||
// check if the calling thread (alias UI thread) is interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
writeThisToFile();
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for all threads are suspended before save the state
|
||||
if (threads != null) runAsync(-1, this::selfPause);
|
||||
init = null;
|
||||
pauseThreads();
|
||||
}
|
||||
|
||||
private void selfPause() {
|
||||
try {
|
||||
for (Thread thread : threads) {
|
||||
if (thread.isAlive()) {
|
||||
thread.interrupt();
|
||||
thread.join(5000);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
writeThisToFile();
|
||||
}
|
||||
private void pauseThreads() {
|
||||
running = false;
|
||||
joinForThreads(-1);
|
||||
writeThisToFile();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -527,9 +510,10 @@ public class DownloadMission extends Mission {
|
||||
*/
|
||||
@Override
|
||||
public boolean delete() {
|
||||
deleted = true;
|
||||
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_DELETED);
|
||||
|
||||
boolean res = deleteThisFromFile();
|
||||
|
||||
if (!super.delete()) return false;
|
||||
@ -544,35 +528,37 @@ public class DownloadMission extends Mission {
|
||||
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
|
||||
*/
|
||||
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
|
||||
done = 0;
|
||||
length = 0;
|
||||
errCode = errorCode;
|
||||
errObject = null;
|
||||
unknownLength = false;
|
||||
threads = null;
|
||||
threads = new Thread[0];
|
||||
fallbackResumeOffset = 0;
|
||||
blocks = null;
|
||||
blockAcquired = null;
|
||||
|
||||
if (rollback) current = 0;
|
||||
|
||||
if (persistChanges)
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (persistChanges) writeThisToFile();
|
||||
}
|
||||
|
||||
private void initializer() {
|
||||
init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this));
|
||||
}
|
||||
|
||||
private void writeThisToFileAsync() {
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this {@link DownloadMission} to the meta file asynchronously
|
||||
* if no thread is already running.
|
||||
*/
|
||||
void writeThisToFile() {
|
||||
synchronized (LOCK) {
|
||||
if (deleted) return;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (metadata == null) return;
|
||||
Utility.writeToFile(metadata, this);
|
||||
writingToFile = false;
|
||||
}
|
||||
mWritingToFile = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -625,11 +611,10 @@ public class DownloadMission extends Mission {
|
||||
public long getLength() {
|
||||
long calculated;
|
||||
if (psState == 1 || psState == 3) {
|
||||
calculated = length;
|
||||
} else {
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
return length;
|
||||
}
|
||||
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
calculated -= offsets[0];// don't count reserved space
|
||||
|
||||
return calculated > nearLength ? calculated : nearLength;
|
||||
@ -642,7 +627,7 @@ public class DownloadMission extends Mission {
|
||||
*/
|
||||
public void setEnqueued(boolean queue) {
|
||||
enqueued = queue;
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
writeThisToFileAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -681,24 +666,19 @@ public class DownloadMission extends Mission {
|
||||
* @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false}
|
||||
*/
|
||||
public boolean isRecovering() {
|
||||
return threads != null && threads.length > 0 && threads[0] instanceof DownloadRunnable && threads[0].isAlive();
|
||||
return threads.length > 0 && threads[0] instanceof DownloadMissionRecover && threads[0].isAlive();
|
||||
}
|
||||
|
||||
private boolean doPostprocessing() {
|
||||
if (psAlgorithm == null || psState == 2) return true;
|
||||
|
||||
private void doPostprocessing() {
|
||||
errCode = ERROR_NOTHING;
|
||||
errObject = null;
|
||||
Thread thread = Thread.currentThread();
|
||||
|
||||
notifyPostProcessing(1);
|
||||
notifyProgress(0);
|
||||
|
||||
if (DEBUG)
|
||||
Thread.currentThread().setName("[" + TAG + "] ps = " +
|
||||
psAlgorithm.getClass().getSimpleName() +
|
||||
" filename = " + storage.getName()
|
||||
);
|
||||
|
||||
threads = new Thread[]{Thread.currentThread()};
|
||||
if (DEBUG) {
|
||||
thread.setName("[" + TAG + "] ps = " + psAlgorithm + " filename = " + storage.getName());
|
||||
}
|
||||
|
||||
Exception exception = null;
|
||||
|
||||
@ -707,6 +687,11 @@ public class DownloadMission extends Mission {
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||
|
||||
if (err instanceof InterruptedIOException || err instanceof ClosedByInterruptException || thread.isInterrupted()) {
|
||||
notifyError(DownloadMission.ERROR_POSTPROCESSING_STOPPED, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||
|
||||
exception = err;
|
||||
@ -717,56 +702,38 @@ public class DownloadMission extends Mission {
|
||||
if (errCode != ERROR_NOTHING) {
|
||||
if (exception == null) exception = errObject;
|
||||
notifyError(ERROR_POSTPROCESSING, exception);
|
||||
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
notifyFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover the download
|
||||
*
|
||||
* @param fromError exception which require update the url from the source
|
||||
* @param errorCode error code which trigger the recovery procedure
|
||||
*/
|
||||
void doRecover(Exception fromError) {
|
||||
void doRecover(int errorCode) {
|
||||
Log.i(TAG, "Attempting to recover the mission: " + storage.getName());
|
||||
|
||||
if (recoveryInfo == null) {
|
||||
if (fromError == null)
|
||||
notifyError(ERROR_RESOURCE_GONE, null);
|
||||
else
|
||||
notifyError(fromError);
|
||||
|
||||
notifyError(errorCode, null);
|
||||
urls = new String[0];// mark this mission as dead
|
||||
return;
|
||||
}
|
||||
|
||||
if (threads != null) {
|
||||
for (Thread thread : threads) {
|
||||
if (thread == Thread.currentThread()) continue;
|
||||
thread.interrupt();
|
||||
joinForThread(thread, 0);
|
||||
}
|
||||
}
|
||||
|
||||
errCode = ERROR_NOTHING;
|
||||
errObject = null;
|
||||
|
||||
if (recoveryInfo[current].attempts >= maxRetry) {
|
||||
recoveryInfo[current].attempts = 0;
|
||||
notifyError(fromError);
|
||||
return;
|
||||
}
|
||||
joinForThreads(0);
|
||||
|
||||
threads = new Thread[]{
|
||||
runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, fromError))
|
||||
runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode))
|
||||
};
|
||||
}
|
||||
|
||||
private boolean deleteThisFromFile() {
|
||||
synchronized (LOCK) {
|
||||
return metadata.delete();
|
||||
boolean res = metadata.delete();
|
||||
metadata = null;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@ -776,8 +743,8 @@ public class DownloadMission extends Mission {
|
||||
* @param id id of new thread (used for debugging only)
|
||||
* @param who the Runnable whose {@code run} method is invoked.
|
||||
*/
|
||||
private void runAsync(int id, Runnable who) {
|
||||
runAsync(id, new Thread(who));
|
||||
private Thread runAsync(int id, Runnable who) {
|
||||
return runAsync(id, new Thread(who));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -806,28 +773,44 @@ public class DownloadMission extends Mission {
|
||||
/**
|
||||
* Waits at most {@code millis} milliseconds for the thread to die
|
||||
*
|
||||
* @param thread the desired thread
|
||||
* @param millis the time to wait in milliseconds
|
||||
*/
|
||||
private void joinForThread(Thread thread, int millis) {
|
||||
if (thread == null || !thread.isAlive()) return;
|
||||
if (thread == Thread.currentThread()) return;
|
||||
private void joinForThreads(int millis) {
|
||||
final Thread currentThread = Thread.currentThread();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "a thread is !still alive!: " + thread.getName());
|
||||
if (init != null && init != currentThread && init.isAlive()) {
|
||||
init.interrupt();
|
||||
|
||||
if (millis > 0) {
|
||||
try {
|
||||
init.join(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Initializer thread is still running", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// still alive, this should not happen.
|
||||
// Possible reasons:
|
||||
// if a thread is still alive, possible reasons:
|
||||
// slow device
|
||||
// the user is spamming start/pause buttons
|
||||
// start() method called quickly after pause()
|
||||
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive() || thread == Thread.currentThread()) continue;
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
try {
|
||||
thread.join(millis);
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive()) continue;
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "thread alive: " + thread.getName());
|
||||
}
|
||||
if (millis > 0) thread.join(millis);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "timeout on join : " + thread.getName());
|
||||
throw new RuntimeException("A thread is still running:\n" + thread.getName());
|
||||
throw new RuntimeException("A download thread is still running", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
@ -15,7 +16,8 @@ import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.List;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||
|
||||
public class DownloadMissionRecover extends Thread {
|
||||
@ -23,47 +25,67 @@ public class DownloadMissionRecover extends Thread {
|
||||
static final int mID = -3;
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final Exception mFromError;
|
||||
private final boolean notInitialized;
|
||||
private final boolean mNotInitialized;
|
||||
|
||||
private final int mErrCode;
|
||||
|
||||
private HttpURLConnection mConn;
|
||||
private MissionRecoveryInfo mRecovery;
|
||||
private StreamExtractor mExtractor;
|
||||
|
||||
DownloadMissionRecover(DownloadMission mission, Exception originError) {
|
||||
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
||||
mMission = mission;
|
||||
mFromError = originError;
|
||||
notInitialized = mission.blocks == null && mission.current == 0;
|
||||
mNotInitialized = mission.blocks == null && mission.current == 0;
|
||||
mErrCode = errCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMission.source == null) {
|
||||
mMission.notifyError(mFromError);
|
||||
mMission.notifyError(mErrCode, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception err = null;
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt++ < mMission.maxRetry) {
|
||||
try {
|
||||
tryRecover();
|
||||
return;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
// give up
|
||||
mMission.notifyError(mErrCode, err);
|
||||
}
|
||||
|
||||
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
||||
/*if (mMission.source.startsWith(MissionRecoveryInfo.DIRECT_SOURCE)) {
|
||||
resolve(mMission.source.substring(MissionRecoveryInfo.DIRECT_SOURCE.length()));
|
||||
return;
|
||||
}*/
|
||||
|
||||
try {
|
||||
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
||||
mExtractor = svr.getStreamExtractor(mMission.source);
|
||||
mExtractor.fetchPage();
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
if (mExtractor == null) {
|
||||
try {
|
||||
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
||||
mExtractor = svr.getStreamExtractor(mMission.source);
|
||||
mExtractor.fetchPage();
|
||||
} catch (ExtractionException e) {
|
||||
mExtractor = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// maybe the following check is redundant
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (!notInitialized) {
|
||||
if (!mNotInitialized) {
|
||||
// set the current download url to null in case if the recovery
|
||||
// process is canceled. Next time start() method is called the
|
||||
// recovery will be executed, saving time
|
||||
@ -87,7 +109,7 @@ public class DownloadMissionRecover extends Thread {
|
||||
if (!mMission.running) return;
|
||||
|
||||
// before continue, check if the current stream was resolved
|
||||
if (mMission.urls[mMission.current] == null || mMission.errCode != ERROR_NOTHING) {
|
||||
if (mMission.urls[mMission.current] == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -103,59 +125,54 @@ public class DownloadMissionRecover extends Thread {
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private void resolveStream() {
|
||||
if (mExtractor.getErrorMessage() != null) {
|
||||
mMission.notifyError(mFromError);
|
||||
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
||||
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
||||
/*if (mExtractor.getErrorMessage() != null) {
|
||||
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
||||
return;
|
||||
}*/
|
||||
|
||||
String url = null;
|
||||
|
||||
switch (mRecovery.kind) {
|
||||
case 'a':
|
||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
||||
if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) {
|
||||
url = audio.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
List<VideoStream> videoStreams;
|
||||
if (mRecovery.desired2)
|
||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||
else
|
||||
videoStreams = mExtractor.getVideoStreams();
|
||||
for (VideoStream video : videoStreams) {
|
||||
if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) {
|
||||
url = video.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) {
|
||||
String tag = subtitles.getLanguageTag();
|
||||
if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) {
|
||||
url = subtitles.getURL();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown stream type");
|
||||
}
|
||||
|
||||
try {
|
||||
String url = null;
|
||||
|
||||
switch (mRecovery.kind) {
|
||||
case 'a':
|
||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
||||
if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) {
|
||||
url = audio.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
List<VideoStream> videoStreams;
|
||||
if (mRecovery.desired2)
|
||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||
else
|
||||
videoStreams = mExtractor.getVideoStreams();
|
||||
for (VideoStream video : videoStreams) {
|
||||
if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) {
|
||||
url = video.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) {
|
||||
String tag = subtitles.getLanguageTag();
|
||||
if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) {
|
||||
url = subtitles.getURL();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown stream type");
|
||||
}
|
||||
|
||||
resolve(url);
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
mRecovery.attempts++;
|
||||
mMission.notifyError(e);
|
||||
}
|
||||
resolve(url);
|
||||
}
|
||||
|
||||
private void resolve(String url) throws IOException, DownloadMission.HttpError {
|
||||
private void resolve(String url) throws IOException, HttpError {
|
||||
if (mRecovery.validateCondition == null) {
|
||||
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
||||
}
|
||||
@ -190,10 +207,7 @@ public class DownloadMissionRecover extends Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DownloadMission.HttpError(code);
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
throw e;
|
||||
throw new HttpError(code);
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
@ -205,14 +219,14 @@ public class DownloadMissionRecover extends Thread {
|
||||
);
|
||||
|
||||
mMission.urls[mMission.current] = url;
|
||||
mRecovery.attempts = 0;
|
||||
|
||||
if (url == null) {
|
||||
mMission.urls = new String[0];
|
||||
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (notInitialized) return;
|
||||
if (mNotInitialized) return;
|
||||
|
||||
if (stale) {
|
||||
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
||||
|
@ -87,6 +87,7 @@ public class DownloadRunnable extends Thread {
|
||||
if (mConn.getResponseCode() == 416) {
|
||||
if (block.done > 0) {
|
||||
// try again from the start (of the block)
|
||||
mMission.notifyProgress(-block.done);
|
||||
block.done = 0;
|
||||
retry = true;
|
||||
mConn.disconnect();
|
||||
@ -114,7 +115,7 @@ public class DownloadRunnable extends Thread {
|
||||
int len;
|
||||
|
||||
// use always start <= end
|
||||
// fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly
|
||||
// fixes a deadlock because in some videos, youtube is sending one byte alone
|
||||
while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
|
||||
f.write(buf, 0, len);
|
||||
start += len;
|
||||
@ -135,7 +136,7 @@ public class DownloadRunnable extends Thread {
|
||||
|
||||
if (mId == 1) {
|
||||
// only the first thread will execute the recovery procedure
|
||||
mMission.doRecover(e);
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -47,22 +48,10 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (mF != null) mF.close();
|
||||
}
|
||||
|
||||
private long loadPosition() {
|
||||
synchronized (mMission.LOCK) {
|
||||
return mMission.fallbackResumeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private void savePosition(long position) {
|
||||
synchronized (mMission.LOCK) {
|
||||
mMission.fallbackResumeOffset = position;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean done;
|
||||
long start = loadPosition();
|
||||
long start = mMission.fallbackResumeOffset;
|
||||
|
||||
if (DEBUG && !mMission.unknownLength && start > 0) {
|
||||
Log.i(TAG, "Resuming a single-thread download at " + start);
|
||||
@ -83,6 +72,7 @@ public class DownloadRunnableFallback extends Thread {
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416 && start > 0) {
|
||||
mMission.notifyProgress(-start);
|
||||
start = 0;
|
||||
mRetryCount--;
|
||||
throw new DownloadMission.HttpError(416);
|
||||
@ -92,6 +82,11 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
if (mMission.unknownLength || mConn.getResponseCode() == 200) {
|
||||
// restart amount of bytes downloaded
|
||||
mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0];
|
||||
}
|
||||
|
||||
mF = mMission.storage.getStream();
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
@ -113,14 +108,14 @@ public class DownloadRunnableFallback extends Thread {
|
||||
} catch (Exception e) {
|
||||
dispose();
|
||||
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
dispose();
|
||||
mMission.doRecover(e);
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -140,7 +135,7 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (done) {
|
||||
mMission.notifyFinished();
|
||||
} else {
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,17 +2,17 @@ package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class FinishedMission extends Mission {
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
public FinishedMission(@NonNull DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
length = mission.length;
|
||||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ package us.shandian.giga.get;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
@ -23,8 +24,6 @@ public class MissionRecoveryInfo implements Serializable, Parcelable {
|
||||
byte kind;
|
||||
String validateCondition = null;
|
||||
|
||||
transient int attempts = 0;
|
||||
|
||||
public MissionRecoveryInfo(@NonNull Stream stream) {
|
||||
if (stream instanceof AudioStream) {
|
||||
desiredBitrate = ((AudioStream) stream).average_bitrate;
|
||||
@ -51,7 +50,7 @@ public class MissionRecoveryInfo implements Serializable, Parcelable {
|
||||
public String toString() {
|
||||
String info;
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("type=");
|
||||
str.append("{type=");
|
||||
switch (kind) {
|
||||
case 'a':
|
||||
str.append("audio");
|
||||
@ -73,7 +72,8 @@ public class MissionRecoveryInfo implements Serializable, Parcelable {
|
||||
str.append(" format=")
|
||||
.append(format.getName())
|
||||
.append(' ')
|
||||
.append(info);
|
||||
.append(info)
|
||||
.append('}');
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
private static final int REPORT_INTERVAL = 256 * 1024;
|
||||
|
||||
private SharpStream source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||
this(target, start, target.length());
|
||||
}
|
||||
private long progressReport;
|
||||
private final ProgressReport onProgress;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException {
|
||||
source = target;
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
onProgress = callback;
|
||||
progressReport = REPORT_INTERVAL;
|
||||
|
||||
if (length < 1) {
|
||||
source.close();
|
||||
@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if ((position + len) > length) {
|
||||
len = (int) (length - position);
|
||||
}
|
||||
@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream {
|
||||
int res = source.read(b, off, len);
|
||||
position += res;
|
||||
|
||||
if (onProgress != null && position > progressReport) {
|
||||
onProgress.report(position);
|
||||
progressReport = position + REPORT_INTERVAL;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[]) throws IOException {
|
||||
public void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
if (onProgress != null) {
|
||||
onProgress.report(-out.length - aux.length);// rollback the whole progress
|
||||
onProgress.report(0);// rollback the whole progress
|
||||
}
|
||||
|
||||
seek(0);
|
||||
@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream {
|
||||
long check();
|
||||
}
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
|
||||
public interface WriteErrorHandle {
|
||||
|
||||
/**
|
||||
@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream {
|
||||
|
||||
class BufferedFile {
|
||||
|
||||
protected final SharpStream target;
|
||||
final SharpStream target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
long length;
|
||||
|
||||
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
private int queueSize;
|
||||
@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void close() {
|
||||
void close() {
|
||||
queue = null;
|
||||
target.close();
|
||||
}
|
||||
|
||||
protected void write(byte b[], int off, int len) throws IOException {
|
||||
void write(byte[] b, int off, int len) throws IOException {
|
||||
while (len > 0) {
|
||||
// if the queue is full, the method available() will flush the queue
|
||||
int read = Math.min(available(), len);
|
||||
@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected int available() throws IOException {
|
||||
int available() throws IOException {
|
||||
if (queueSize >= queue.length) {
|
||||
flush();
|
||||
return queue.length;
|
||||
@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
void seek(long absoluteOffset) throws IOException {
|
||||
if (absoluteOffset == offset) {
|
||||
return;// nothing to do
|
||||
}
|
||||
|
11
app/src/main/java/us/shandian/giga/io/ProgressReport.java
Normal file
11
app/src/main/java/us/shandian/giga/io/ProgressReport.java
Normal file
@ -0,0 +1,11 @@
|
||||
package us.shandian.giga.io;
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.OggFromWebMWriter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
@ -1,9 +1,9 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.io.CircularFileWriter;
|
||||
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.io.ProgressReport;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
|
||||
public abstract class Postprocessing implements Serializable {
|
||||
|
||||
@ -63,22 +63,22 @@ public abstract class Postprocessing implements Serializable {
|
||||
* Get a boolean value that indicate if the given algorithm work on the same
|
||||
* file
|
||||
*/
|
||||
public final boolean worksOnSameFile;
|
||||
public boolean worksOnSameFile;
|
||||
|
||||
/**
|
||||
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
|
||||
*/
|
||||
public final boolean reserveSpace;
|
||||
public boolean reserveSpace;
|
||||
|
||||
/**
|
||||
* Gets the given algorithm short name
|
||||
*/
|
||||
private final String name;
|
||||
private String name;
|
||||
|
||||
|
||||
private String[] args;
|
||||
|
||||
protected transient DownloadMission mission;
|
||||
private transient DownloadMission mission;
|
||||
|
||||
private File tempFile;
|
||||
|
||||
@ -109,16 +109,24 @@ public abstract class Postprocessing implements Serializable {
|
||||
long finalLength = -1;
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = mission.storage.length();
|
||||
|
||||
long length = mission.storage.length() - mission.offsets[0];
|
||||
mission.length = length > mission.nearLength ? length : mission.nearLength;
|
||||
|
||||
final ProgressReport readProgress = (long position) -> {
|
||||
position -= mission.offsets[0];
|
||||
if (position > mission.done) mission.done = position;
|
||||
};
|
||||
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||
for (int i = 0, j = 1; i < sources.length; i++, j++) {
|
||||
SharpStream source = mission.storage.getStream();
|
||||
long end = j < sources.length ? mission.offsets[j] : source.length();
|
||||
|
||||
sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress);
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||
|
||||
if (test(sources)) {
|
||||
for (SharpStream source : sources) source.rewind();
|
||||
@ -140,7 +148,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
};
|
||||
|
||||
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
|
||||
out.onProgress = this::progressReport;
|
||||
out.onProgress = (long position) -> mission.done = position;
|
||||
|
||||
out.onWriteError = (err) -> {
|
||||
mission.psState = 3;
|
||||
@ -187,11 +195,10 @@ public abstract class Postprocessing implements Serializable {
|
||||
|
||||
if (result == OK_RESULT) {
|
||||
if (finalLength != -1) {
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
}
|
||||
} else {
|
||||
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errCode = ERROR_POSTPROCESSING;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
@ -229,23 +236,12 @@ public abstract class Postprocessing implements Serializable {
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
mission.done = done;
|
||||
if (mission.length < mission.done) mission.length = mission.done;
|
||||
|
||||
Message m = new Message();
|
||||
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
||||
m.obj = mission;
|
||||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append("name=").append(name).append('[');
|
||||
str.append("{ name=").append(name).append('[');
|
||||
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
@ -255,6 +251,6 @@ public abstract class Postprocessing implements Serializable {
|
||||
str.delete(0, 1);
|
||||
}
|
||||
|
||||
return str.append(']').toString();
|
||||
return str.append("] }").toString();
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,11 @@ package us.shandian.giga.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -152,6 +150,8 @@ public class DownloadManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
mis.threads = new Thread[0];
|
||||
|
||||
boolean exists;
|
||||
try {
|
||||
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||
@ -170,8 +170,6 @@ public class DownloadManager {
|
||||
// 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());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
|
||||
mis.psState = 0;
|
||||
@ -243,7 +241,6 @@ public class DownloadManager {
|
||||
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||
|
||||
if (canDownloadInCurrentNetwork() && start) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
@ -252,7 +249,6 @@ public class DownloadManager {
|
||||
|
||||
public void resumeMission(DownloadMission mission) {
|
||||
if (!mission.running) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
@ -261,7 +257,6 @@ public class DownloadManager {
|
||||
if (mission.running) {
|
||||
mission.setEnqueued(false);
|
||||
mission.pause();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,7 +269,6 @@ public class DownloadManager {
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.delete();
|
||||
}
|
||||
}
|
||||
@ -291,7 +285,6 @@ public class DownloadManager {
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.storage = null;
|
||||
mission.delete();
|
||||
}
|
||||
@ -374,35 +367,29 @@ public class DownloadManager {
|
||||
}
|
||||
|
||||
public void pauseAllMissions(boolean force) {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
|
||||
|
||||
if (force) mission.threads = null;// avoid waiting for threads
|
||||
if (force) {
|
||||
// avoid waiting for threads
|
||||
mission.init = null;
|
||||
mission.threads = new Thread[0];
|
||||
}
|
||||
|
||||
mission.pause();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
public void startAllMissions() {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || mission.isCorrupt()) continue;
|
||||
|
||||
flag = true;
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -483,28 +470,18 @@ public class DownloadManager {
|
||||
|
||||
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
|
||||
|
||||
int running = 0;
|
||||
int paused = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.isCorrupt() || mission.isPsRunning()) continue;
|
||||
|
||||
if (mission.running && isMetered) {
|
||||
paused++;
|
||||
mission.pause();
|
||||
} else if (!mission.running && !isMetered && mission.enqueued) {
|
||||
running++;
|
||||
mission.start();
|
||||
if (mPrefQueueLimit) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (running > 0) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
return;
|
||||
}
|
||||
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
void updateMaximumAttempts() {
|
||||
@ -513,22 +490,6 @@ public class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast check for pending downloads. If exists, the user will be notified
|
||||
* TODO: call this method in somewhere
|
||||
*
|
||||
* @param context the application context
|
||||
*/
|
||||
public static void notifyUserPendingDownloads(Context context) {
|
||||
int pending = getPendingDir(context).list().length;
|
||||
if (pending < 1) return;
|
||||
|
||||
Toast.makeText(context, context.getString(
|
||||
R.string.msg_pending_downloads,
|
||||
String.valueOf(pending)
|
||||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(storage);
|
||||
|
@ -25,14 +25,15 @@ import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
@ -41,8 +42,6 @@ import org.schabi.newpipe.player.helper.LockManager;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
@ -58,11 +57,11 @@ public class DownloadManagerService extends Service {
|
||||
|
||||
private static final String TAG = "DownloadManagerService";
|
||||
|
||||
public static final int MESSAGE_RUNNING = 0;
|
||||
public static final int MESSAGE_PAUSED = 1;
|
||||
public static final int MESSAGE_FINISHED = 2;
|
||||
public static final int MESSAGE_PROGRESS = 3;
|
||||
public static final int MESSAGE_ERROR = 4;
|
||||
public static final int MESSAGE_DELETED = 5;
|
||||
public static final int MESSAGE_ERROR = 3;
|
||||
public static final int MESSAGE_DELETED = 4;
|
||||
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1000;
|
||||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||
@ -217,9 +216,11 @@ public class DownloadManagerService extends Service {
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -250,6 +251,7 @@ public class DownloadManagerService extends Service {
|
||||
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
||||
if (icLauncher != null) icLauncher.recycle();
|
||||
|
||||
mHandler = null;
|
||||
mManager.pauseAllMissions(true);
|
||||
}
|
||||
|
||||
@ -274,6 +276,8 @@ public class DownloadManagerService extends Service {
|
||||
}
|
||||
|
||||
private boolean handleMessage(@NonNull Message msg) {
|
||||
if (mHandler == null) return true;
|
||||
|
||||
DownloadMission mission = (DownloadMission) msg.obj;
|
||||
|
||||
switch (msg.what) {
|
||||
@ -284,7 +288,7 @@ public class DownloadManagerService extends Service {
|
||||
handleConnectivityState(false);
|
||||
updateForegroundState(mManager.runMissions());
|
||||
break;
|
||||
case MESSAGE_PROGRESS:
|
||||
case MESSAGE_RUNNING:
|
||||
updateForegroundState(true);
|
||||
break;
|
||||
case MESSAGE_ERROR:
|
||||
@ -300,11 +304,8 @@ public class DownloadManagerService extends Service {
|
||||
if (msg.what != MESSAGE_ERROR)
|
||||
mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
|
||||
|
||||
synchronized (mEchoObservers) {
|
||||
for (Callback observer : mEchoObservers) {
|
||||
observer.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
for (Callback observer : mEchoObservers)
|
||||
observer.handleMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -523,16 +524,6 @@ public class DownloadManagerService extends Service {
|
||||
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private void manageObservers(Callback handler, boolean add) {
|
||||
synchronized (mEchoObservers) {
|
||||
if (add) {
|
||||
mEchoObservers.add(handler);
|
||||
} else {
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void manageLock(boolean acquire) {
|
||||
if (acquire == mLockAcquired) return;
|
||||
|
||||
@ -605,11 +596,11 @@ public class DownloadManagerService extends Service {
|
||||
}
|
||||
|
||||
public void addMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, true);
|
||||
mEchoObservers.add(handler);
|
||||
}
|
||||
|
||||
public void removeMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, false);
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
|
||||
public void clearDownloadNotifications() {
|
||||
|
@ -10,16 +10,6 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@ -34,6 +24,17 @@ import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@ -82,6 +83,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
private static final String TAG = "MissionAdapter";
|
||||
private static final String UNDEFINED_PROGRESS = "--.-%";
|
||||
private static final String DEFAULT_MIME_TYPE = "*/*";
|
||||
private static final String UNDEFINED_ETA = "--:--";
|
||||
|
||||
|
||||
static {
|
||||
@ -103,10 +105,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
private View mEmptyMessage;
|
||||
private RecoverHelper mRecover;
|
||||
|
||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
|
||||
private final Runnable rUpdater = this::updater;
|
||||
|
||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
|
||||
mContext = context;
|
||||
mDownloadManager = downloadManager;
|
||||
mDeleter = null;
|
||||
|
||||
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mLayout = R.layout.mission_item;
|
||||
@ -117,7 +120,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
mIterator = downloadManager.getIterator();
|
||||
|
||||
mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
|
||||
checkEmptyMessageVisibility();
|
||||
onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -142,17 +148,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
if (h.item.mission instanceof DownloadMission) {
|
||||
mPendingDownloadsItems.remove(h);
|
||||
if (mPendingDownloadsItems.size() < 1) {
|
||||
setAutoRefresh(false);
|
||||
checkMasterButtonsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
h.popupMenu.dismiss();
|
||||
h.item = null;
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
h.lastCurrent = -1;
|
||||
h.state = 0;
|
||||
h.resetSpeedMeasure();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -191,7 +193,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
h.size.setText(length);
|
||||
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
||||
h.lastCurrent = mission.current;
|
||||
updateProgress(h);
|
||||
mPendingDownloadsItems.add(h);
|
||||
} else {
|
||||
@ -216,20 +217,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
private void updateProgress(ViewHolderItem h) {
|
||||
if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
DownloadMission mission = (DownloadMission) h.item.mission;
|
||||
|
||||
if (h.lastCurrent != mission.current) {
|
||||
h.lastCurrent = mission.current;
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = 0;
|
||||
} else {
|
||||
if (h.lastTimeStamp == -1) h.lastTimeStamp = now;
|
||||
if (h.lastDone == -1) h.lastDone = mission.done;
|
||||
}
|
||||
|
||||
long deltaTime = now - h.lastTimeStamp;
|
||||
long deltaDone = mission.done - h.lastDone;
|
||||
double done = mission.done;
|
||||
long length = mission.getLength();
|
||||
long now = System.currentTimeMillis();
|
||||
boolean hasError = mission.errCode != ERROR_NOTHING;
|
||||
|
||||
// hide on error
|
||||
@ -237,19 +228,16 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
// show if length is unknown
|
||||
h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength));
|
||||
|
||||
float progress;
|
||||
double progress;
|
||||
if (mission.unknownLength) {
|
||||
progress = Float.NaN;
|
||||
progress = Double.NaN;
|
||||
h.progress.setProgress(0f);
|
||||
} else {
|
||||
progress = (float) ((double) mission.done / mission.length);
|
||||
if (mission.urls.length > 1 && mission.current < mission.urls.length) {
|
||||
progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length);
|
||||
}
|
||||
progress = done / length;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1d : progress);
|
||||
h.status.setText(R.string.msg_error);
|
||||
} else if (isNotFinite(progress)) {
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
@ -258,59 +246,78 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
h.progress.setProgress(progress);
|
||||
}
|
||||
|
||||
long length = mission.getLength();
|
||||
@StringRes int state;
|
||||
String sizeStr = Utility.formatBytes(length).concat(" ");
|
||||
|
||||
int state;
|
||||
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
|
||||
state = 0;
|
||||
h.size.setText(sizeStr);
|
||||
return;
|
||||
} else if (!mission.running) {
|
||||
state = mission.enqueued ? 1 : 2;
|
||||
state = mission.enqueued ? R.string.queued : R.string.paused;
|
||||
} else if (mission.isPsRunning()) {
|
||||
state = 3;
|
||||
state = R.string.post_processing;
|
||||
} else if (mission.isRecovering()) {
|
||||
state = R.string.recovering;
|
||||
} else {
|
||||
state = 0;
|
||||
}
|
||||
|
||||
if (state != 0) {
|
||||
// update state without download speed
|
||||
if (h.state != state) {
|
||||
String statusStr;
|
||||
h.state = state;
|
||||
h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
|
||||
h.resetSpeedMeasure();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
statusStr = mContext.getString(R.string.queued);
|
||||
break;
|
||||
case 2:
|
||||
statusStr = mContext.getString(R.string.paused);
|
||||
break;
|
||||
case 3:
|
||||
statusStr = mContext.getString(R.string.post_processing);
|
||||
break;
|
||||
default:
|
||||
statusStr = "?";
|
||||
break;
|
||||
}
|
||||
if (h.lastTimestamp < 0) {
|
||||
h.size.setText(sizeStr);
|
||||
h.lastTimestamp = now;
|
||||
h.lastDone = done;
|
||||
return;
|
||||
}
|
||||
|
||||
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")"));
|
||||
} else if (deltaDone > 0) {
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = mission.done;
|
||||
}
|
||||
long deltaTime = now - h.lastTimestamp;
|
||||
double deltaDone = done - h.lastDone;
|
||||
|
||||
if (h.lastDone > done) {
|
||||
h.lastDone = done;
|
||||
h.size.setText(sizeStr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaDone > 0 && deltaTime > 0) {
|
||||
float speed = (deltaDone * 1000f) / deltaTime;
|
||||
float speed = (float) ((deltaDone * 1000d) / deltaTime);
|
||||
float averageSpeed = speed;
|
||||
|
||||
String speedStr = Utility.formatSpeed(speed);
|
||||
String sizeStr = Utility.formatBytes(length);
|
||||
if (h.lastSpeedIdx < 0) {
|
||||
for (int i = 0; i < h.lastSpeed.length; i++) {
|
||||
h.lastSpeed[i] = speed;
|
||||
}
|
||||
h.lastSpeedIdx = 0;
|
||||
} else {
|
||||
for (int i = 0; i < h.lastSpeed.length; i++) {
|
||||
averageSpeed += h.lastSpeed[i];
|
||||
}
|
||||
averageSpeed /= h.lastSpeed.length + 1f;
|
||||
}
|
||||
|
||||
h.size.setText(sizeStr.concat(" ").concat(speedStr));
|
||||
String speedStr = Utility.formatSpeed(averageSpeed);
|
||||
String etaStr;
|
||||
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = mission.done;
|
||||
if (mission.unknownLength) {
|
||||
etaStr = "";
|
||||
} else {
|
||||
long eta = (long) Math.ceil((length - done) / averageSpeed);
|
||||
etaStr = " @ ".concat(Utility.stringifySeconds(eta));
|
||||
}
|
||||
|
||||
h.size.setText(sizeStr.concat(speedStr).concat(etaStr));
|
||||
|
||||
h.lastTimestamp = now;
|
||||
h.lastDone = done;
|
||||
h.lastSpeed[h.lastSpeedIdx++] = speed;
|
||||
|
||||
if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,6 +396,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
return true;
|
||||
}
|
||||
|
||||
private ViewHolderItem getViewHolder(Object mission) {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (h.item.mission == mission) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(@NonNull Message msg) {
|
||||
if (mStartButton != null && mPauseButton != null) {
|
||||
@ -396,33 +410,28 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
}
|
||||
|
||||
switch (msg.what) {
|
||||
case DownloadManagerService.MESSAGE_PROGRESS:
|
||||
case DownloadManagerService.MESSAGE_ERROR:
|
||||
case DownloadManagerService.MESSAGE_FINISHED:
|
||||
case DownloadManagerService.MESSAGE_DELETED:
|
||||
case DownloadManagerService.MESSAGE_PAUSED:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
|
||||
setAutoRefresh(true);
|
||||
return true;
|
||||
}
|
||||
ViewHolderItem h = getViewHolder(msg.obj);
|
||||
if (h == null) return false;
|
||||
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (h.item.mission != msg.obj) continue;
|
||||
|
||||
if (msg.what == DownloadManagerService.MESSAGE_FINISHED) {
|
||||
switch (msg.what) {
|
||||
case DownloadManagerService.MESSAGE_FINISHED:
|
||||
case DownloadManagerService.MESSAGE_DELETED:
|
||||
// DownloadManager should mark the download as finished
|
||||
applyChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
updateProgress(h);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
updateProgress(h);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showError(@NonNull DownloadMission mission) {
|
||||
@ -470,8 +479,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
msg = R.string.error_insufficient_storage;
|
||||
break;
|
||||
case ERROR_UNKNOWN_EXCEPTION:
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
return;
|
||||
if (mission.errObject != null) {
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
return;
|
||||
} else {
|
||||
msg = R.string.msg_error;
|
||||
break;
|
||||
}
|
||||
case ERROR_PROGRESS_LOST:
|
||||
msg = R.string.error_progress_lost;
|
||||
break;
|
||||
@ -521,7 +535,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
request.append(" [");
|
||||
if (mission.recoveryInfo != null) {
|
||||
for (MissionRecoveryInfo recovery : mission.recoveryInfo)
|
||||
request.append(" {").append(recovery.toString()).append("} ");
|
||||
request.append(' ')
|
||||
.append(recovery.toString())
|
||||
.append(' ');
|
||||
}
|
||||
request.append("]");
|
||||
|
||||
@ -556,16 +572,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
switch (id) {
|
||||
case R.id.start:
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
mDownloadManager.resumeMission(mission);
|
||||
return true;
|
||||
case R.id.pause:
|
||||
h.state = -1;
|
||||
mDownloadManager.pauseMission(mission);
|
||||
updateProgress(h);
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
return true;
|
||||
case R.id.error_message_view:
|
||||
showError(mission);
|
||||
@ -598,12 +608,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
shareFile(h.item.mission);
|
||||
return true;
|
||||
case R.id.delete:
|
||||
if (mDeleter == null) {
|
||||
mDownloadManager.deleteMission(h.item.mission);
|
||||
} else {
|
||||
mDeleter.append(h.item.mission);
|
||||
}
|
||||
mDeleter.append(h.item.mission);
|
||||
applyChanges();
|
||||
checkMasterButtonsVisibility();
|
||||
return true;
|
||||
case R.id.md5:
|
||||
case R.id.sha1:
|
||||
@ -639,7 +646,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
mIterator.end();
|
||||
|
||||
for (ViewHolderItem item : mPendingDownloadsItems) {
|
||||
item.lastTimeStamp = -1;
|
||||
item.resetSpeedMeasure();
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
@ -672,6 +679,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
public void checkMasterButtonsVisibility() {
|
||||
boolean[] state = mIterator.hasValidPendingMissions();
|
||||
Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]);
|
||||
setButtonVisible(mPauseButton, state[0]);
|
||||
setButtonVisible(mStartButton, state[1]);
|
||||
}
|
||||
@ -681,86 +689,57 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
button.setVisible(visible);
|
||||
}
|
||||
|
||||
public void ensurePausedMissions() {
|
||||
public void refreshMissionItems() {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (((DownloadMission) h.item.mission).running) continue;
|
||||
updateProgress(h);
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
h.resetSpeedMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void deleterDispose(boolean commitChanges) {
|
||||
if (mDeleter != null) mDeleter.dispose(commitChanges);
|
||||
public void onDestroy() {
|
||||
mDeleter.dispose();
|
||||
}
|
||||
|
||||
public void deleterLoad(View view) {
|
||||
if (mDeleter == null)
|
||||
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
public void onResume() {
|
||||
mDeleter.resume();
|
||||
mHandler.post(rUpdater);
|
||||
}
|
||||
|
||||
public void deleterResume() {
|
||||
if (mDeleter != null) mDeleter.resume();
|
||||
}
|
||||
|
||||
public void recoverMission(DownloadMission mission) {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (mission != h.item.mission) continue;
|
||||
|
||||
mission.errObject = null;
|
||||
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
|
||||
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
h.progress.setMarquee(true);
|
||||
|
||||
mDownloadManager.resumeMission(mission);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private boolean mUpdaterRunning = false;
|
||||
private final Runnable rUpdater = this::updater;
|
||||
|
||||
public void onPaused() {
|
||||
setAutoRefresh(false);
|
||||
mDeleter.pause();
|
||||
mHandler.removeCallbacks(rUpdater);
|
||||
}
|
||||
|
||||
private void setAutoRefresh(boolean enabled) {
|
||||
if (enabled && !mUpdaterRunning) {
|
||||
mUpdaterRunning = true;
|
||||
updater();
|
||||
} else if (!enabled && mUpdaterRunning) {
|
||||
mUpdaterRunning = false;
|
||||
mHandler.removeCallbacks(rUpdater);
|
||||
}
|
||||
|
||||
public void recoverMission(DownloadMission mission) {
|
||||
ViewHolderItem h = getViewHolder(mission);
|
||||
if (h == null) return;
|
||||
|
||||
mission.errObject = null;
|
||||
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
|
||||
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
h.progress.setMarquee(true);
|
||||
|
||||
mDownloadManager.resumeMission(mission);
|
||||
}
|
||||
|
||||
private void updater() {
|
||||
if (!mUpdaterRunning) return;
|
||||
|
||||
boolean running = false;
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
// check if the mission is running first
|
||||
if (!((DownloadMission) h.item.mission).running) continue;
|
||||
|
||||
updateProgress(h);
|
||||
running = true;
|
||||
}
|
||||
|
||||
if (running) {
|
||||
mHandler.postDelayed(rUpdater, 1000);
|
||||
} else {
|
||||
mUpdaterRunning = false;
|
||||
}
|
||||
mHandler.postDelayed(rUpdater, 1000);
|
||||
}
|
||||
|
||||
private boolean isNotFinite(Float value) {
|
||||
return Float.isNaN(value) || Float.isInfinite(value);
|
||||
private boolean isNotFinite(double value) {
|
||||
return Double.isNaN(value) || Double.isInfinite(value);
|
||||
}
|
||||
|
||||
public void setRecover(@NonNull RecoverHelper callback) {
|
||||
@ -789,10 +768,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
MenuItem source;
|
||||
MenuItem checksum;
|
||||
|
||||
long lastTimeStamp = -1;
|
||||
long lastDone = -1;
|
||||
int lastCurrent = -1;
|
||||
int state = 0;
|
||||
long lastTimestamp = -1;
|
||||
double lastDone;
|
||||
int lastSpeedIdx;
|
||||
float[] lastSpeed = new float[3];
|
||||
String estimatedTimeArrival = UNDEFINED_ETA;
|
||||
|
||||
ViewHolderItem(View view) {
|
||||
super(view);
|
||||
@ -902,6 +882,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
private void resetSpeedMeasure() {
|
||||
estimatedTimeArrival = UNDEFINED_ETA;
|
||||
lastTimestamp = -1;
|
||||
lastSpeedIdx = -1;
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolderHeader extends RecyclerView.ViewHolder {
|
||||
|
@ -4,9 +4,10 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -113,7 +114,7 @@ public class Deleter {
|
||||
show();
|
||||
}
|
||||
|
||||
private void pause() {
|
||||
public void pause() {
|
||||
running = false;
|
||||
mHandler.removeCallbacks(rNext);
|
||||
mHandler.removeCallbacks(rShow);
|
||||
@ -126,13 +127,11 @@ public class Deleter {
|
||||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||
}
|
||||
|
||||
public void dispose(boolean commitChanges) {
|
||||
public void dispose() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
|
||||
if (!commitChanges) return;
|
||||
|
||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||
items = null;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable {
|
||||
mForegroundColor = foreground;
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
mProgress = progress;
|
||||
public void setProgress(double progress) {
|
||||
mProgress = (float) progress;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,6 @@ import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@ -24,6 +19,12 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@ -72,8 +73,7 @@ public class MissionsFragment extends Fragment {
|
||||
mBinder = (DownloadManagerBinder) binder;
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
||||
mAdapter.deleterLoad(getView());
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView());
|
||||
|
||||
mAdapter.setRecover(MissionsFragment.this::recoverMission);
|
||||
|
||||
@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment {
|
||||
* Added in API level 23.
|
||||
*/
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Bug: in api< 23 this is never called
|
||||
@ -147,7 +147,7 @@ public class MissionsFragment extends Fragment {
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
public void onAttach(@NonNull Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
mContext = activity;
|
||||
@ -162,7 +162,7 @@ public class MissionsFragment extends Fragment {
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mBinder.enableNotifications(true);
|
||||
mContext.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(true);
|
||||
mAdapter.onDestroy();
|
||||
|
||||
mBinder = null;
|
||||
mAdapter = null;
|
||||
@ -196,13 +196,11 @@ public class MissionsFragment extends Fragment {
|
||||
prompt.create().show();
|
||||
return true;
|
||||
case R.id.start_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().startAllMissions();
|
||||
return true;
|
||||
case R.id.pause_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||
mAdapter.ensurePausedMissions();// update items view
|
||||
mAdapter.refreshMissionItems();// update items view
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@ -271,23 +269,12 @@ public class MissionsFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterDispose(false);
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterResume();
|
||||
mAdapter.onResume();
|
||||
|
||||
if (mForceUpdate) {
|
||||
mForceUpdate = false;
|
||||
@ -303,7 +290,13 @@ public class MissionsFragment extends Fragment {
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (mAdapter != null) mAdapter.onPaused();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mAdapter.onPaused();
|
||||
}
|
||||
|
||||
if (mBinder != null) mBinder.enableNotifications(true);
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,14 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
@ -26,6 +27,7 @@ import java.io.Serializable;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
@ -39,26 +41,28 @@ public class Utility {
|
||||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (bytes < 1024) {
|
||||
return String.format("%d B", bytes);
|
||||
return String.format(locale, "%d B", bytes);
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f kB", bytes / 1024d);
|
||||
return String.format(locale, "%.2f kB", bytes / 1024d);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", bytes / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f MB", bytes / 1024d / 1024d);
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatSpeed(float speed) {
|
||||
public static String formatSpeed(double speed) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (speed < 1024) {
|
||||
return String.format("%.2f B/s", speed);
|
||||
return String.format(locale, "%.2f B/s", speed);
|
||||
} else if (speed < 1024 * 1024) {
|
||||
return String.format("%.2f kB/s", speed / 1024);
|
||||
return String.format(locale, "%.2f kB/s", speed / 1024);
|
||||
} else if (speed < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB/s", speed / 1024 / 1024);
|
||||
return String.format(locale, "%.2f MB/s", speed / 1024 / 1024);
|
||||
} else {
|
||||
return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,12 +192,11 @@ public class Utility {
|
||||
switch (type) {
|
||||
case MUSIC:
|
||||
return R.drawable.music;
|
||||
default:
|
||||
case VIDEO:
|
||||
return R.drawable.video;
|
||||
case SUBTITLE:
|
||||
return R.drawable.subtitle;
|
||||
default:
|
||||
return R.drawable.video;
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,4 +277,25 @@ public class Utility {
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String pad(int number) {
|
||||
return number < 10 ? ("0" + number) : String.valueOf(number);
|
||||
}
|
||||
|
||||
public static String stringifySeconds(double seconds) {
|
||||
int h = (int) Math.floor(seconds / 3600);
|
||||
int m = (int) Math.floor((seconds - (h * 3600)) / 60);
|
||||
int s = (int) (seconds - (h * 3600) - (m * 60));
|
||||
|
||||
String str = "";
|
||||
|
||||
if (h < 1 && m < 1) {
|
||||
str = "00:";
|
||||
} else {
|
||||
if (h > 0) str = pad(h) + ":";
|
||||
if (m > 0) str += pad(m) + ":";
|
||||
}
|
||||
|
||||
return str + pad(s);
|
||||
}
|
||||
}
|
||||
|
@ -471,7 +471,6 @@
|
||||
<string name="error_http_not_found">غير موجود</string>
|
||||
<string name="error_postprocessing_failed">فشلت المعالجة الاولية</string>
|
||||
<string name="clear_finished_download">حذف التنزيلات المنتهية</string>
|
||||
<string name="msg_pending_downloads">"قم بإستكمال %s حيثما يتم التحويل من التنزيلات"</string>
|
||||
<string name="stop">توقف</string>
|
||||
<string name="max_retry_msg">أقصى عدد للمحاولات</string>
|
||||
<string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string>
|
||||
|
@ -458,7 +458,6 @@
|
||||
<string name="error_http_not_found">Не знойдзена</string>
|
||||
<string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string>
|
||||
<string name="clear_finished_download">Ачысціць завершаныя</string>
|
||||
<string name="msg_pending_downloads">Аднавіць прыпыненыя загрузкі (%s)</string>
|
||||
<string name="stop">Спыніць</string>
|
||||
<string name="max_retry_msg">Максімум спробаў</string>
|
||||
<string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string>
|
||||
|
@ -460,7 +460,6 @@
|
||||
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
|
||||
<string name="error_path_creation">无法创建目标文件夹</string>
|
||||
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试</string>
|
||||
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>
|
||||
<string name="pause_downloads_on_mobile_desc">切换至移动数据时有用,尽管一些下载无法被暂停</string>
|
||||
<string name="show_comments_title">显示评论</string>
|
||||
<string name="show_comments_summary">禁用停止显示评论</string>
|
||||
|
@ -466,7 +466,6 @@ otevření ve vyskakovacím okně</string>
|
||||
<string name="error_http_not_found">Nenalezeno</string>
|
||||
<string name="error_postprocessing_failed">Post-processing selhal</string>
|
||||
<string name="clear_finished_download">Vyčistit dokončená stahování</string>
|
||||
<string name="msg_pending_downloads">Pokračovat ve stahování %s souborů, čekajících na stažení</string>
|
||||
<string name="stop">Zastavit</string>
|
||||
<string name="max_retry_msg">Maximální počet pokusů o opakování</string>
|
||||
<string name="max_retry_desc">Maximální počet pokusů před zrušením stahování</string>
|
||||
|
@ -447,7 +447,6 @@
|
||||
<string name="paused">sat på pause</string>
|
||||
<string name="queued">sat i kø</string>
|
||||
<string name="clear_finished_download">Ryd færdige downloads</string>
|
||||
<string name="msg_pending_downloads">Fortsæt dine %s ventende overførsler fra Downloads</string>
|
||||
<string name="max_retry_msg">Maksimalt antal genforsøg</string>
|
||||
<string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</string>
|
||||
<string name="pause_downloads_on_mobile">Sæt på pause ved skift til mobildata</string>
|
||||
|
@ -457,7 +457,6 @@
|
||||
<string name="error_http_not_found">Nicht gefunden</string>
|
||||
<string name="error_postprocessing_failed">Nachbearbeitung fehlgeschlagen</string>
|
||||
<string name="clear_finished_download">Um fertige Downloads bereinigen</string>
|
||||
<string name="msg_pending_downloads">Setze deine %s ausstehenden Übertragungen von Downloads fort</string>
|
||||
<string name="stop">Anhalten</string>
|
||||
<string name="max_retry_msg">Maximale Wiederholungen</string>
|
||||
<string name="max_retry_desc">Maximalanzahl der Versuche, bevor der Download abgebrochen wird</string>
|
||||
|
@ -459,7 +459,6 @@
|
||||
<string name="error_http_not_found">Δεν βρέθηκε</string>
|
||||
<string name="error_postprocessing_failed">Μετεπεξεργασία απέτυχε</string>
|
||||
<string name="clear_finished_download">Εκκαθάριση ολοκληρωμένων λήψεων</string>
|
||||
<string name="msg_pending_downloads">Συνέχιση των %s εκκρεμών σας λήψεων</string>
|
||||
<string name="stop">Διακοπή</string>
|
||||
<string name="max_retry_msg">Μέγιστες επαναπροσπάθειες</string>
|
||||
<string name="max_retry_desc">Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης</string>
|
||||
|
@ -406,6 +406,7 @@
|
||||
<string name="paused">pausado</string>
|
||||
<string name="queued">en cola</string>
|
||||
<string name="post_processing">posprocesamiento</string>
|
||||
<string name="recovering">recuperando</string>
|
||||
<string name="enqueue">Añadir a cola</string>
|
||||
<string name="permission_denied">Acción denegada por el sistema</string>
|
||||
<string name="file_deleted">Se eliminó el archivo</string>
|
||||
@ -424,7 +425,6 @@
|
||||
<string name="grid">Mostrar como grilla</string>
|
||||
<string name="list">Mostrar como lista</string>
|
||||
<string name="clear_finished_download">Limpiar descargas finalizadas</string>
|
||||
<string name="msg_pending_downloads">Tienes %s descargas pendientes, ve a Descargas para continuarlas</string>
|
||||
<string name="confirm_prompt">¿Lo confirma\?</string>
|
||||
<string name="stop">Detener</string>
|
||||
<string name="max_retry_msg">Intentos máximos</string>
|
||||
|
@ -460,7 +460,6 @@
|
||||
<string name="error_http_not_found">Ei leitud</string>
|
||||
<string name="error_postprocessing_failed">Järeltöötlemine nurjus</string>
|
||||
<string name="clear_finished_download">Eemalda lõpetatud allalaadimised</string>
|
||||
<string name="msg_pending_downloads">Jätka %s pooleliolevat allalaadimist</string>
|
||||
<string name="stop">Stopp</string>
|
||||
<string name="max_retry_msg">Korduskatseid</string>
|
||||
<string name="max_retry_desc">Suurim katsete arv enne allalaadimise tühistamist</string>
|
||||
|
@ -459,7 +459,6 @@
|
||||
<string name="error_http_not_found">Ez aurkitua</string>
|
||||
<string name="error_postprocessing_failed">Post-prozesuak huts egin du</string>
|
||||
<string name="clear_finished_download">Garbitu amaitutako deskargak</string>
|
||||
<string name="msg_pending_downloads">Berrekin burutzeke dauden %s transferentzia deskargetatik</string>
|
||||
<string name="stop">Gelditu</string>
|
||||
<string name="max_retry_msg">Gehienezko saiakerak</string>
|
||||
<string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string>
|
||||
|
@ -466,7 +466,6 @@
|
||||
<string name="max_retry_desc">Nombre maximum de tentatives avant d’annuler le téléchargement</string>
|
||||
<string name="saved_tabs_invalid_json">Utilisation des onglets par défaut, erreur lors de la lecture des onglets enregistrés</string>
|
||||
<string name="error_http_unsupported_range">Le serveur n’accepte pas les téléchargements multi-fils, veuillez réessayer avec @string/msg_threads = 1</string>
|
||||
<string name="msg_pending_downloads">Continuer vos %s transferts en attente depuis Téléchargement</string>
|
||||
<string name="show_comments_title">Afficher les commentaires</string>
|
||||
<string name="show_comments_summary">Désactiver pour ne pas afficher les commentaires</string>
|
||||
<string name="autoplay_title">Lecture automatique</string>
|
||||
|
@ -464,7 +464,6 @@
|
||||
<string name="error_http_not_found">לא נמצא</string>
|
||||
<string name="error_postprocessing_failed">העיבוד המאוחר נכשל</string>
|
||||
<string name="clear_finished_download">פינוי ההורדות שהסתיימו</string>
|
||||
<string name="msg_pending_downloads">ניתן להמשיך את %s ההורדות הממתינות שלך דרך ההורדות</string>
|
||||
<string name="stop">עצירה</string>
|
||||
<string name="max_retry_msg">מספר הניסיונות החוזרים המרבי</string>
|
||||
<string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string>
|
||||
|
@ -457,7 +457,6 @@
|
||||
<string name="error_http_not_found">Nije pronađeno</string>
|
||||
<string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string>
|
||||
<string name="clear_finished_download">Obriši završena preuzimanja</string>
|
||||
<string name="msg_pending_downloads">Nastavite s prijenosima na čekanju za %s s preuzimanja</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maksimalnih ponovnih pokušaja</string>
|
||||
<string name="max_retry_desc">Maksimalni broj pokušaja prije poništavanja preuzimanja</string>
|
||||
|
@ -453,7 +453,6 @@
|
||||
<string name="error_http_not_found">Tidak ditemukan</string>
|
||||
<string name="error_postprocessing_failed">Pengolahan-pasca gagal</string>
|
||||
<string name="clear_finished_download">Hapus unduhan yang sudah selesai</string>
|
||||
<string name="msg_pending_downloads">Lanjutkan %s transfer anda yang tertunda dari Unduhan</string>
|
||||
<string name="stop">Berhenti</string>
|
||||
<string name="max_retry_msg">Percobaan maksimum</string>
|
||||
<string name="max_retry_desc">Jumlah upaya maksimum sebelum membatalkan unduhan</string>
|
||||
|
@ -457,7 +457,6 @@
|
||||
<string name="error_http_not_found">Non trovato</string>
|
||||
<string name="error_postprocessing_failed">Post-processing fallito</string>
|
||||
<string name="clear_finished_download">Pulisci i download completati</string>
|
||||
<string name="msg_pending_downloads">Continua i %s trasferimenti in corso dai Download</string>
|
||||
<string name="stop">Ferma</string>
|
||||
<string name="max_retry_msg">Tentativi massimi</string>
|
||||
<string name="max_retry_desc">Tentativi massimi prima di cancellare il download</string>
|
||||
|
@ -456,7 +456,6 @@
|
||||
<string name="saved_tabs_invalid_json">デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました</string>
|
||||
<string name="main_page_content_summary">メインページに表示されるタブ</string>
|
||||
<string name="updates_setting_description">新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します</string>
|
||||
<string name="msg_pending_downloads">ダウンロードから %s の保留中の転送を続行します</string>
|
||||
<string name="pause_downloads_on_mobile">従量制課金ネットワークの割り込み</string>
|
||||
<string name="pause_downloads_on_mobile_desc">モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません</string>
|
||||
<string name="show_comments_title">コメントを表示</string>
|
||||
|
@ -454,7 +454,6 @@
|
||||
<string name="error_http_not_found">HTTP 찾을 수 없습니다</string>
|
||||
<string name="error_postprocessing_failed">후처리 작업이 실패하였습니다</string>
|
||||
<string name="clear_finished_download">완료된 다운로드 비우기</string>
|
||||
<string name="msg_pending_downloads">대기중인 %s 다운로드를 지속하세요</string>
|
||||
<string name="stop">멈추기</string>
|
||||
<string name="max_retry_msg">최대 재시도 횟수</string>
|
||||
<string name="max_retry_desc">다운로드를 취소하기 전까지 다시 시도할 최대 횟수</string>
|
||||
|
@ -453,7 +453,6 @@
|
||||
<string name="error_http_not_found">Tidak ditemui</string>
|
||||
<string name="error_postprocessing_failed">Pemprosesan-pasca gagal</string>
|
||||
<string name="clear_finished_download">Hapuskan senarai muat turun yang selesai</string>
|
||||
<string name="msg_pending_downloads">Teruskan %s pemindahan anda yang menunggu dari muat turun</string>
|
||||
<string name="stop">Berhenti</string>
|
||||
<string name="max_retry_msg">Percubaan maksimum</string>
|
||||
<string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string>
|
||||
|
@ -458,7 +458,6 @@
|
||||
<string name="error_http_not_found">Ikke funnet</string>
|
||||
<string name="error_postprocessing_failed">Etterbehandling mislyktes</string>
|
||||
<string name="clear_finished_download">Tøm fullførte nedlastinger</string>
|
||||
<string name="msg_pending_downloads">Fortsett dine %s ventende overføringer fra Nedlastinger</string>
|
||||
<string name="stop">Stopp</string>
|
||||
<string name="max_retry_msg">Maksimalt antall forsøk</string>
|
||||
<string name="max_retry_desc">Maksimalt antall tilkoblingsforsøk før nedlastingen avblåses</string>
|
||||
|
@ -457,7 +457,6 @@
|
||||
<string name="error_http_not_found">Niet gevonden</string>
|
||||
<string name="error_postprocessing_failed">Nabewerking mislukt</string>
|
||||
<string name="clear_finished_download">Voltooide downloads wissen</string>
|
||||
<string name="msg_pending_downloads">Zet uw %s wachtende downloads verder via Downloads</string>
|
||||
<string name="stop">Stoppen</string>
|
||||
<string name="max_retry_msg">Maximaal aantal pogingen</string>
|
||||
<string name="max_retry_desc">Maximaal aantal pogingen vooraleer dat den download wordt geannuleerd</string>
|
||||
|
@ -457,7 +457,6 @@
|
||||
<string name="error_http_not_found">Niet gevonden</string>
|
||||
<string name="error_postprocessing_failed">Nabewerking mislukt</string>
|
||||
<string name="clear_finished_download">Voltooide downloads wissen</string>
|
||||
<string name="msg_pending_downloads">Zet je %s wachtende downloads voort via Downloads</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum aantal keer proberen</string>
|
||||
<string name="max_retry_desc">Maximum aantal pogingen voordat de download wordt geannuleerd</string>
|
||||
|
@ -453,7 +453,6 @@
|
||||
<string name="error_http_not_found">ਨਹੀਂ ਲਭਿਆ</string>
|
||||
<string name="error_postprocessing_failed">Post-processing ਫੇਲ੍ਹ</string>
|
||||
<string name="clear_finished_download">ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string>
|
||||
<string name="msg_pending_downloads">ਡਾਉਨਲੋਡਸ ਤੋਂ ਆਪਣੀਆਂ %s ਬਕਾਇਆ ਟ੍ਰਾਂਸਫਰ ਜਾਰੀ ਰੱਖੋ</string>
|
||||
<string name="stop">ਰੁੱਕੋ</string>
|
||||
<string name="max_retry_msg">ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string>
|
||||
<string name="max_retry_desc">ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string>
|
||||
|
@ -459,7 +459,6 @@
|
||||
<string name="error_http_not_found">Nie znaleziono</string>
|
||||
<string name="error_postprocessing_failed">Przetwarzanie końcowe nie powiodło się</string>
|
||||
<string name="clear_finished_download">Wyczyść ukończone pobieranie</string>
|
||||
<string name="msg_pending_downloads">Kontynuuj %s oczekujące transfery z plików do pobrania</string>
|
||||
<string name="stop">Zatrzymaj</string>
|
||||
<string name="max_retry_msg">Maksymalna liczba powtórzeń</string>
|
||||
<string name="max_retry_desc">Maksymalna liczba prób przed anulowaniem pobierania</string>
|
||||
|
@ -466,7 +466,6 @@ abrir em modo popup</string>
|
||||
<string name="error_http_not_found">Não encontrado</string>
|
||||
<string name="error_postprocessing_failed">Falha no pós processamento</string>
|
||||
<string name="clear_finished_download">Limpar downloads finalizados</string>
|
||||
<string name="msg_pending_downloads">Continuar seus %s downloads pendentes</string>
|
||||
<string name="stop">Parar</string>
|
||||
<string name="max_retry_msg">Tentativas Máximas</string>
|
||||
<string name="max_retry_desc">Número máximo de tentativas antes de cancelar o download</string>
|
||||
|
@ -455,7 +455,6 @@
|
||||
<string name="error_http_not_found">Não encontrado</string>
|
||||
<string name="error_postprocessing_failed">Pós-processamento falhado</string>
|
||||
<string name="clear_finished_download">Limpar transferências concluídas</string>
|
||||
<string name="msg_pending_downloads">Continue as suas %s transferências pendentes das Transferências</string>
|
||||
<string name="stop">Parar</string>
|
||||
<string name="max_retry_msg">Tentativas máximas</string>
|
||||
<string name="max_retry_desc">Número máximo de tentativas antes de cancelar a transferência</string>
|
||||
|
@ -464,7 +464,6 @@
|
||||
<string name="download_finished">Загрузка завершена</string>
|
||||
<string name="download_finished_more">%s загрузок завершено</string>
|
||||
<string name="generate_unique_name">Создать уникальное имя</string>
|
||||
<string name="msg_pending_downloads">Возобновить приостановленные загрузки (%s)</string>
|
||||
<string name="max_retry_msg">Максимум попыток</string>
|
||||
<string name="max_retry_desc">Количество попыток перед отменой загрузки</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Некоторые загрузки не поддерживают докачку и начнутся с начала</string>
|
||||
|
@ -465,7 +465,6 @@
|
||||
<string name="error_http_not_found">Nenájdené</string>
|
||||
<string name="error_postprocessing_failed">Post-spracovanie zlyhalo</string>
|
||||
<string name="clear_finished_download">Vyčistiť dokončené sťahovania</string>
|
||||
<string name="msg_pending_downloads">Pokračujte v preberaní %s zo súborov na prevzatie</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum opakovaní</string>
|
||||
<string name="max_retry_desc">Maximálny počet pokusov pred zrušením stiahnutia</string>
|
||||
|
@ -452,7 +452,6 @@
|
||||
<string name="error_http_not_found">Bulunamadı</string>
|
||||
<string name="error_postprocessing_failed">İşlem sonrası başarısız</string>
|
||||
<string name="clear_finished_download">Tamamlanan indirmeleri temizle</string>
|
||||
<string name="msg_pending_downloads">Beklemedeki %s transferinize İndirmeler\'den devam edin</string>
|
||||
<string name="stop">Durdur</string>
|
||||
<string name="max_retry_msg">Azami deneme sayısı</string>
|
||||
<string name="max_retry_desc">İndirmeyi iptal etmeden önce maksimum deneme sayısı</string>
|
||||
|
@ -471,7 +471,6 @@
|
||||
<string name="saved_tabs_invalid_json">Помилка зчитування збережених вкладок. Використовую типові вкладки.</string>
|
||||
<string name="main_page_content_summary">Вкладки, що відображаються на головній сторінці</string>
|
||||
<string name="updates_setting_description">Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії</string>
|
||||
<string name="msg_pending_downloads">Продовжити ваші %s відкладених переміщень із Завантажень</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені</string>
|
||||
<string name="show_comments_title">Показувати коментарі</string>
|
||||
<string name="show_comments_summary">Вимнути відображення дописів</string>
|
||||
|
@ -452,7 +452,6 @@
|
||||
<string name="error_http_not_found">Không tìm thấy</string>
|
||||
<string name="error_postprocessing_failed">Xử lý thất bại</string>
|
||||
<string name="clear_finished_download">Dọn các tải về đã hoàn thành</string>
|
||||
<string name="msg_pending_downloads">Hãy tiếp tục %s tải về đang chờ</string>
|
||||
<string name="stop">Dừng</string>
|
||||
<string name="max_retry_msg">Số lượt thử lại tối đa</string>
|
||||
<string name="max_retry_desc">Số lượt thử lại trước khi hủy tải về</string>
|
||||
|
@ -450,7 +450,6 @@
|
||||
<string name="error_http_not_found">找不到</string>
|
||||
<string name="error_postprocessing_failed">後處理失敗</string>
|
||||
<string name="clear_finished_download">清除已結束的下載</string>
|
||||
<string name="msg_pending_downloads">繼續從您所擱置中的下載 %s 傳輸</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="max_retry_msg">最大重試次數</string>
|
||||
<string name="max_retry_desc">在取消下載前的最大嘗試數</string>
|
||||
|
@ -526,6 +526,7 @@
|
||||
<string name="paused">paused</string>
|
||||
<string name="queued">queued</string>
|
||||
<string name="post_processing">post-processing</string>
|
||||
<string name="recovering">recovering</string>
|
||||
<string name="enqueue">Queue</string>
|
||||
<string name="permission_denied">Action denied by the system</string>
|
||||
<!-- download notifications -->
|
||||
@ -560,7 +561,6 @@
|
||||
<string name="error_download_resource_gone">Cannot recover this download</string>
|
||||
<string name="clear_finished_download">Clear finished downloads</string>
|
||||
<string name="confirm_prompt">Are you sure?</string>
|
||||
<string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum retries</string>
|
||||
<string name="max_retry_desc">Maximum number of attempts before canceling the download</string>
|
||||
|
Loading…
Reference in New Issue
Block a user