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:
kapodamy 2019-10-09 23:49:23 -03:00
parent 763995d4c9
commit e6d9d8e26d
53 changed files with 554 additions and 622 deletions

View File

@ -2,6 +2,15 @@ package org.schabi.newpipe.fragments;
import android.content.Context; import android.content.Context;
import android.os.Bundle; 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.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;

View File

@ -1,6 +1,6 @@
package org.schabi.newpipe.streams; 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.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment; import org.schabi.newpipe.streams.WebMReader.Segment;

View File

@ -1,9 +1,10 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import androidx.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException; 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) { if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
// for youtube streams. The url has expired // for youtube streams. The url has expired
interrupt(); interrupt();
mMission.doRecover(e); mMission.doRecover(ERROR_HTTP_FORBIDDEN);
return; return;
} }

View File

@ -4,18 +4,21 @@ import android.os.Handler;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.Serializable; import java.io.Serializable;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.channels.ClosedByInterruptException;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
@ -27,7 +30,7 @@ import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission extends Mission { 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 BUFFER_SIZE = 64 * 1024;
static final int BLOCK_SIZE = 512 * 1024; static final int BLOCK_SIZE = 512 * 1024;
@ -61,9 +64,9 @@ public class DownloadMission extends Mission {
public String[] urls; 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 * 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} * 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 * Maximum of download threads running, chosen by the user
@ -132,22 +135,23 @@ public class DownloadMission extends Mission {
public MissionRecoveryInfo[] recoveryInfo; public MissionRecoveryInfo[] recoveryInfo;
private transient int finishCount; private transient int finishCount;
public transient boolean running; public transient volatile boolean running;
public boolean enqueued; public boolean enqueued;
public int errCode = ERROR_NOTHING; public int errCode = ERROR_NOTHING;
public Exception errObject = null; public Exception errObject = null;
public transient Handler mHandler; public transient Handler mHandler;
private transient boolean mWritingToFile;
private transient boolean[] blockAcquired; private transient boolean[] blockAcquired;
private transient long writingToFileNext;
private transient volatile boolean writingToFile;
final Object LOCK = new Lock(); final Object LOCK = new Lock();
private transient boolean deleted; @NonNull
public transient Thread[] threads = new Thread[0];
public transient volatile Thread[] threads = new Thread[0]; public transient Thread init = null;
private transient Thread init = null;
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
if (urls == null) throw new NullPointerException("urls is null"); if (urls == null) throw new NullPointerException("urls is null");
@ -246,8 +250,10 @@ public class DownloadMission extends Mission {
int statusCode = conn.getResponseCode(); int statusCode = conn.getResponseCode();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, threadId + ":Range=" + conn.getRequestProperty("Range")); Log.d(TAG, threadId + ":[request] Range=" + conn.getRequestProperty("Range"));
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode); 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) { synchronized void notifyProgress(long deltaLen) {
if (!running) return;
if (unknownLength) { if (unknownLength) {
length += deltaLen;// Update length before proceeding length += deltaLen;// Update length before proceeding
} }
done += deltaLen; done += deltaLen;
if (done > length) { if (metadata == null) return;
done = length;
}
if (done != length && !deleted && !mWritingToFile) { if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) {
mWritingToFile = true; writingToFile = true;
runAsync(-2, this::writeThisToFile); writingToFileNext = done + BLOCK_SIZE;
writeThisToFileAsync();
} }
notify(DownloadManagerService.MESSAGE_PROGRESS);
} }
synchronized void notifyError(Exception err) { synchronized void notifyError(Exception err) {
@ -342,43 +343,42 @@ public class DownloadMission extends Mission {
notify(DownloadManagerService.MESSAGE_ERROR); notify(DownloadManagerService.MESSAGE_ERROR);
if (running) { if (running) pauseThreads();
running = false;
if (threads != null) selfPause();
}
} }
synchronized void notifyFinished() { synchronized void notifyFinished() {
if (errCode > ERROR_NOTHING) return; if (current < urls.length) {
if (++finishCount < threads.length) return;
finishCount++;
if (blocks.length < 1 || threads == null || finishCount == threads.length) {
if (errCode != ERROR_NOTHING) return;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length); Log.d(TAG, "onFinish: downloaded " + (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;
} }
current++; current++;
unknownLength = false; if (current < urls.length) {
// prepare next sub-mission
if (!doPostprocessing()) return; offsets[current] = offsets[current - 1] + length;
initializer();
enqueued = false; return;
running = false; }
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
} }
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) { private void notifyPostProcessing(int state) {
@ -396,10 +396,15 @@ public class DownloadMission extends Mission {
Log.d(TAG, action + " postprocessing on " + storage.getName()); Log.d(TAG, action + " postprocessing on " + storage.getName());
if (state == 2) {
psState = state;
return;
}
synchronized (LOCK) { synchronized (LOCK) {
// don't return without fully write the current state // don't return without fully write the current state
psState = 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; if (running || isFinished() || urls.length < 1) return;
// ensure that the previous state is completely paused. // ensure that the previous state is completely paused.
int maxWait = 10000;// 10 seconds joinForThreads(10000);
joinForThread(init, maxWait);
if (threads != null) {
for (Thread thread : threads) joinForThread(thread, maxWait);
threads = null;
}
running = true; running = true;
errCode = ERROR_NOTHING; errCode = ERROR_NOTHING;
@ -427,12 +427,14 @@ public class DownloadMission extends Mission {
} }
if (current >= urls.length) { if (current >= urls.length) {
runAsync(1, this::notifyFinished); notifyFinished();
return; return;
} }
notify(DownloadManagerService.MESSAGE_RUNNING);
if (urls[current] == null) { if (urls[current] == null) {
doRecover(null); doRecover(ERROR_RESOURCE_GONE);
return; return;
} }
@ -446,18 +448,13 @@ public class DownloadMission extends Mission {
blockAcquired = new boolean[blocks.length]; blockAcquired = new boolean[blocks.length];
if (blocks.length < 1) { if (blocks.length < 1) {
if (unknownLength) {
done = 0;
length = 0;
}
threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))}; threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))};
} else { } else {
int remainingBlocks = 0; int remainingBlocks = 0;
for (int block : blocks) if (block >= 0) remainingBlocks++; for (int block : blocks) if (block >= 0) remainingBlocks++;
if (remainingBlocks < 1) { if (remainingBlocks < 1) {
runAsync(1, this::notifyFinished); notifyFinished();
return; return;
} }
@ -483,6 +480,7 @@ public class DownloadMission extends Mission {
} }
running = false; running = false;
notify(DownloadManagerService.MESSAGE_PAUSED);
if (init != null && init.isAlive()) { if (init != null && init.isAlive()) {
// NOTE: if start() method is running ¡will no have effect! // 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)."); 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 init = null;
if (Thread.currentThread().isInterrupted()) { pauseThreads();
writeThisToFile();
return;
}
// wait for all threads are suspended before save the state
if (threads != null) runAsync(-1, this::selfPause);
} }
private void selfPause() { private void pauseThreads() {
try { running = false;
for (Thread thread : threads) { joinForThreads(-1);
if (thread.isAlive()) { writeThisToFile();
thread.interrupt();
thread.join(5000);
}
}
} catch (Exception e) {
// nothing to do
} finally {
writeThisToFile();
}
} }
/** /**
@ -527,9 +510,10 @@ public class DownloadMission extends Mission {
*/ */
@Override @Override
public boolean delete() { public boolean delete() {
deleted = true;
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir(); if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
notify(DownloadManagerService.MESSAGE_DELETED);
boolean res = deleteThisFromFile(); boolean res = deleteThisFromFile();
if (!super.delete()) return false; 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} * @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
*/ */
public void resetState(boolean rollback, boolean persistChanges, int errorCode) { public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
done = 0; length = 0;
errCode = errorCode; errCode = errorCode;
errObject = null; errObject = null;
unknownLength = false; unknownLength = false;
threads = null; threads = new Thread[0];
fallbackResumeOffset = 0; fallbackResumeOffset = 0;
blocks = null; blocks = null;
blockAcquired = null; blockAcquired = null;
if (rollback) current = 0; if (rollback) current = 0;
if (persistChanges) writeThisToFile();
if (persistChanges)
Utility.writeToFile(metadata, DownloadMission.this);
} }
private void initializer() { private void initializer() {
init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this)); init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this));
} }
private void writeThisToFileAsync() {
runAsync(-2, this::writeThisToFile);
}
/** /**
* Write this {@link DownloadMission} to the meta file asynchronously * Write this {@link DownloadMission} to the meta file asynchronously
* if no thread is already running. * if no thread is already running.
*/ */
void writeThisToFile() { void writeThisToFile() {
synchronized (LOCK) { synchronized (LOCK) {
if (deleted) return; if (metadata == null) return;
Utility.writeToFile(metadata, DownloadMission.this); Utility.writeToFile(metadata, this);
writingToFile = false;
} }
mWritingToFile = false;
} }
/** /**
@ -625,11 +611,10 @@ public class DownloadMission extends Mission {
public long getLength() { public long getLength() {
long calculated; long calculated;
if (psState == 1 || psState == 3) { if (psState == 1 || psState == 3) {
calculated = length; return length;
} else {
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
} }
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
calculated -= offsets[0];// don't count reserved space calculated -= offsets[0];// don't count reserved space
return calculated > nearLength ? calculated : nearLength; return calculated > nearLength ? calculated : nearLength;
@ -642,7 +627,7 @@ public class DownloadMission extends Mission {
*/ */
public void setEnqueued(boolean queue) { public void setEnqueued(boolean queue) {
enqueued = 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} * @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false}
*/ */
public boolean isRecovering() { 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() { private void doPostprocessing() {
if (psAlgorithm == null || psState == 2) return true; errCode = ERROR_NOTHING;
errObject = null; errObject = null;
Thread thread = Thread.currentThread();
notifyPostProcessing(1); notifyPostProcessing(1);
notifyProgress(0);
if (DEBUG) if (DEBUG) {
Thread.currentThread().setName("[" + TAG + "] ps = " + thread.setName("[" + TAG + "] ps = " + psAlgorithm + " filename = " + storage.getName());
psAlgorithm.getClass().getSimpleName() + }
" filename = " + storage.getName()
);
threads = new Thread[]{Thread.currentThread()};
Exception exception = null; Exception exception = null;
@ -707,6 +687,11 @@ public class DownloadMission extends Mission {
} catch (Exception err) { } catch (Exception err) {
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), 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; if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
exception = err; exception = err;
@ -717,56 +702,38 @@ public class DownloadMission extends Mission {
if (errCode != ERROR_NOTHING) { if (errCode != ERROR_NOTHING) {
if (exception == null) exception = errObject; if (exception == null) exception = errObject;
notifyError(ERROR_POSTPROCESSING, exception); notifyError(ERROR_POSTPROCESSING, exception);
return;
return false;
} }
return true; notifyFinished();
} }
/** /**
* Attempts to recover the download * 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()); Log.i(TAG, "Attempting to recover the mission: " + storage.getName());
if (recoveryInfo == null) { if (recoveryInfo == null) {
if (fromError == null) notifyError(errorCode, null);
notifyError(ERROR_RESOURCE_GONE, null);
else
notifyError(fromError);
urls = new String[0];// mark this mission as dead urls = new String[0];// mark this mission as dead
return; return;
} }
if (threads != null) { joinForThreads(0);
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;
}
threads = new Thread[]{ threads = new Thread[]{
runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, fromError)) runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode))
}; };
} }
private boolean deleteThisFromFile() { private boolean deleteThisFromFile() {
synchronized (LOCK) { 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 id id of new thread (used for debugging only)
* @param who the Runnable whose {@code run} method is invoked. * @param who the Runnable whose {@code run} method is invoked.
*/ */
private void runAsync(int id, Runnable who) { private Thread runAsync(int id, Runnable who) {
runAsync(id, new Thread(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 * Waits at most {@code millis} milliseconds for the thread to die
* *
* @param thread the desired thread
* @param millis the time to wait in milliseconds * @param millis the time to wait in milliseconds
*/ */
private void joinForThread(Thread thread, int millis) { private void joinForThreads(int millis) {
if (thread == null || !thread.isAlive()) return; final Thread currentThread = Thread.currentThread();
if (thread == Thread.currentThread()) return;
if (DEBUG) { if (init != null && init != currentThread && init.isAlive()) {
Log.w(TAG, "a thread is !still alive!: " + thread.getName()); 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. // if a thread is still alive, possible reasons:
// Possible reasons:
// slow device // slow device
// the user is spamming start/pause buttons // the user is spamming start/pause buttons
// start() method called quickly after pause() // start() method called quickly after pause()
for (Thread thread : threads) {
if (!thread.isAlive() || thread == Thread.currentThread()) continue;
thread.interrupt();
}
try { 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) { } catch (InterruptedException e) {
Log.d(TAG, "timeout on join : " + thread.getName()); throw new RuntimeException("A download thread is still running", e);
throw new RuntimeException("A thread is still running:\n" + thread.getName());
} }
} }

View File

@ -4,6 +4,7 @@ import android.util.Log;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; 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.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
@ -15,7 +16,8 @@ import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedByInterruptException;
import java.util.List; 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; import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
public class DownloadMissionRecover extends Thread { public class DownloadMissionRecover extends Thread {
@ -23,47 +25,67 @@ public class DownloadMissionRecover extends Thread {
static final int mID = -3; static final int mID = -3;
private final DownloadMission mMission; private final DownloadMission mMission;
private final Exception mFromError; private final boolean mNotInitialized;
private final boolean notInitialized;
private final int mErrCode;
private HttpURLConnection mConn; private HttpURLConnection mConn;
private MissionRecoveryInfo mRecovery; private MissionRecoveryInfo mRecovery;
private StreamExtractor mExtractor; private StreamExtractor mExtractor;
DownloadMissionRecover(DownloadMission mission, Exception originError) { DownloadMissionRecover(DownloadMission mission, int errCode) {
mMission = mission; mMission = mission;
mFromError = originError; mNotInitialized = mission.blocks == null && mission.current == 0;
notInitialized = mission.blocks == null && mission.current == 0; mErrCode = errCode;
} }
@Override @Override
public void run() { public void run() {
if (mMission.source == null) { if (mMission.source == null) {
mMission.notifyError(mFromError); mMission.notifyError(mErrCode, null);
return; 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)) { /*if (mMission.source.startsWith(MissionRecoveryInfo.DIRECT_SOURCE)) {
resolve(mMission.source.substring(MissionRecoveryInfo.DIRECT_SOURCE.length())); resolve(mMission.source.substring(MissionRecoveryInfo.DIRECT_SOURCE.length()));
return; return;
}*/ }*/
try { if (mExtractor == null) {
StreamingService svr = NewPipe.getServiceByUrl(mMission.source); try {
mExtractor = svr.getStreamExtractor(mMission.source); StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
mExtractor.fetchPage(); mExtractor = svr.getStreamExtractor(mMission.source);
} catch (InterruptedIOException | ClosedByInterruptException e) { mExtractor.fetchPage();
return; } catch (ExtractionException e) {
} catch (Exception e) { mExtractor = null;
if (!mMission.running || super.isInterrupted()) return; throw e;
mMission.notifyError(e); }
return;
} }
// maybe the following check is redundant // maybe the following check is redundant
if (!mMission.running || super.isInterrupted()) return; if (!mMission.running || super.isInterrupted()) return;
if (!notInitialized) { if (!mNotInitialized) {
// set the current download url to null in case if the recovery // set the current download url to null in case if the recovery
// process is canceled. Next time start() method is called the // process is canceled. Next time start() method is called the
// recovery will be executed, saving time // recovery will be executed, saving time
@ -87,7 +109,7 @@ public class DownloadMissionRecover extends Thread {
if (!mMission.running) return; if (!mMission.running) return;
// before continue, check if the current stream was resolved // 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; break;
} }
} }
@ -103,59 +125,54 @@ public class DownloadMissionRecover extends Thread {
mMission.start(); mMission.start();
} }
private void resolveStream() { private void resolveStream() throws IOException, ExtractionException, HttpError {
if (mExtractor.getErrorMessage() != null) { // FIXME: this getErrorMessage() always returns "video is unavailable"
mMission.notifyError(mFromError); /*if (mExtractor.getErrorMessage() != null) {
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
return; 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 { resolve(url);
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);
}
} }
private void resolve(String url) throws IOException, DownloadMission.HttpError { private void resolve(String url) throws IOException, HttpError {
if (mRecovery.validateCondition == null) { if (mRecovery.validateCondition == null) {
Log.w(TAG, "validation condition not defined, the resource can be stale"); Log.w(TAG, "validation condition not defined, the resource can be stale");
} }
@ -190,10 +207,7 @@ public class DownloadMissionRecover extends Thread {
return; return;
} }
throw new DownloadMission.HttpError(code); throw new HttpError(code);
} catch (Exception e) {
if (!mMission.running || e instanceof ClosedByInterruptException) return;
throw e;
} finally { } finally {
disconnect(); disconnect();
} }
@ -205,14 +219,14 @@ public class DownloadMissionRecover extends Thread {
); );
mMission.urls[mMission.current] = url; mMission.urls[mMission.current] = url;
mRecovery.attempts = 0;
if (url == null) { if (url == null) {
mMission.urls = new String[0];
mMission.notifyError(ERROR_RESOURCE_GONE, null); mMission.notifyError(ERROR_RESOURCE_GONE, null);
return; return;
} }
if (notInitialized) return; if (mNotInitialized) return;
if (stale) { if (stale) {
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);

View File

@ -87,6 +87,7 @@ public class DownloadRunnable extends Thread {
if (mConn.getResponseCode() == 416) { if (mConn.getResponseCode() == 416) {
if (block.done > 0) { if (block.done > 0) {
// try again from the start (of the block) // try again from the start (of the block)
mMission.notifyProgress(-block.done);
block.done = 0; block.done = 0;
retry = true; retry = true;
mConn.disconnect(); mConn.disconnect();
@ -114,7 +115,7 @@ public class DownloadRunnable extends Thread {
int len; int len;
// use always start <= end // 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) { while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
f.write(buf, 0, len); f.write(buf, 0, len);
start += len; start += len;
@ -135,7 +136,7 @@ public class DownloadRunnable extends Thread {
if (mId == 1) { if (mId == 1) {
// only the first thread will execute the recovery procedure // only the first thread will execute the recovery procedure
mMission.doRecover(e); mMission.doRecover(ERROR_HTTP_FORBIDDEN);
} }
return; return;
} }

View File

@ -1,8 +1,9 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import androidx.annotation.NonNull;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException; import java.io.IOException;
@ -47,22 +48,10 @@ public class DownloadRunnableFallback extends Thread {
if (mF != null) mF.close(); 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 @Override
public void run() { public void run() {
boolean done; boolean done;
long start = loadPosition(); long start = mMission.fallbackResumeOffset;
if (DEBUG && !mMission.unknownLength && start > 0) { if (DEBUG && !mMission.unknownLength && start > 0) {
Log.i(TAG, "Resuming a single-thread download at " + start); 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 // check if the download can be resumed
if (mConn.getResponseCode() == 416 && start > 0) { if (mConn.getResponseCode() == 416 && start > 0) {
mMission.notifyProgress(-start);
start = 0; start = 0;
mRetryCount--; mRetryCount--;
throw new DownloadMission.HttpError(416); throw new DownloadMission.HttpError(416);
@ -92,6 +82,11 @@ public class DownloadRunnableFallback extends Thread {
if (!mMission.unknownLength) if (!mMission.unknownLength)
mMission.unknownLength = Utility.getContentLength(mConn) == -1; 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 = mMission.storage.getStream();
mF.seek(mMission.offsets[mMission.current] + start); mF.seek(mMission.offsets[mMission.current] + start);
@ -113,14 +108,14 @@ public class DownloadRunnableFallback extends Thread {
} catch (Exception e) { } catch (Exception e) {
dispose(); dispose();
savePosition(start); mMission.fallbackResumeOffset = start;
if (!mMission.running || e instanceof ClosedByInterruptException) return; if (!mMission.running || e instanceof ClosedByInterruptException) return;
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) { if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
// for youtube streams. The url has expired, recover // for youtube streams. The url has expired, recover
dispose(); dispose();
mMission.doRecover(e); mMission.doRecover(ERROR_HTTP_FORBIDDEN);
return; return;
} }
@ -140,7 +135,7 @@ public class DownloadRunnableFallback extends Thread {
if (done) { if (done) {
mMission.notifyFinished(); mMission.notifyFinished();
} else { } else {
savePosition(start); mMission.fallbackResumeOffset = start;
} }
} }

View File

@ -2,17 +2,17 @@ package us.shandian.giga.get;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
public class FinishedMission extends Mission { public class FinishedMission extends Mission {
public FinishedMission() { public FinishedMission() {
} }
public FinishedMission(@NonNull DownloadMission mission) { public FinishedMission(@NonNull DownloadMission mission) {
source = mission.source; source = mission.source;
length = mission.length;// ¿or mission.done? length = mission.length;
timestamp = mission.timestamp; timestamp = mission.timestamp;
kind = mission.kind; kind = mission.kind;
storage = mission.storage; storage = mission.storage;
} }
} }

View File

@ -2,7 +2,8 @@ package us.shandian.giga.get;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
@ -23,8 +24,6 @@ public class MissionRecoveryInfo implements Serializable, Parcelable {
byte kind; byte kind;
String validateCondition = null; String validateCondition = null;
transient int attempts = 0;
public MissionRecoveryInfo(@NonNull Stream stream) { public MissionRecoveryInfo(@NonNull Stream stream) {
if (stream instanceof AudioStream) { if (stream instanceof AudioStream) {
desiredBitrate = ((AudioStream) stream).average_bitrate; desiredBitrate = ((AudioStream) stream).average_bitrate;
@ -51,7 +50,7 @@ public class MissionRecoveryInfo implements Serializable, Parcelable {
public String toString() { public String toString() {
String info; String info;
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
str.append("type="); str.append("{type=");
switch (kind) { switch (kind) {
case 'a': case 'a':
str.append("audio"); str.append("audio");
@ -73,7 +72,8 @@ public class MissionRecoveryInfo implements Serializable, Parcelable {
str.append(" format=") str.append(" format=")
.append(format.getName()) .append(format.getName())
.append(' ') .append(' ')
.append(info); .append(info)
.append('}');
return str.toString(); return str.toString();
} }

View File

@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException; import java.io.IOException;
public class ChunkFileInputStream extends SharpStream { public class ChunkFileInputStream extends SharpStream {
private static final int REPORT_INTERVAL = 256 * 1024;
private SharpStream source; private SharpStream source;
private final long offset; private final long offset;
private final long length; private final long length;
private long position; private long position;
public ChunkFileInputStream(SharpStream target, long start) throws IOException { private long progressReport;
this(target, start, target.length()); 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; source = target;
offset = start; offset = start;
length = end - start; length = end - start;
position = 0; position = 0;
onProgress = callback;
progressReport = REPORT_INTERVAL;
if (length < 1) { if (length < 1) {
source.close(); source.close();
@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream {
} }
@Override @Override
public int read(byte b[]) throws IOException { public int read(byte[] b) throws IOException {
return read(b, 0, b.length); return read(b, 0, b.length);
} }
@Override @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) { if ((position + len) > length) {
len = (int) (length - position); len = (int) (length - position);
} }
@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream {
int res = source.read(b, off, len); int res = source.read(b, off, len);
position += res; position += res;
if (onProgress != null && position > progressReport) {
onProgress.report(position);
progressReport = position + REPORT_INTERVAL;
}
return res; return res;
} }

View File

@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream {
} }
@Override @Override
public void write(byte b[]) throws IOException { public void write(byte[] b) throws IOException {
write(b, 0, b.length); write(b, 0, b.length);
} }
@Override @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) { if (len == 0) {
return; return;
} }
@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream {
@Override @Override
public void rewind() throws IOException { public void rewind() throws IOException {
if (onProgress != null) { if (onProgress != null) {
onProgress.report(-out.length - aux.length);// rollback the whole progress onProgress.report(0);// rollback the whole progress
} }
seek(0); seek(0);
@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream {
long check(); long check();
} }
public interface ProgressReport {
/**
* Report the size of the new file
*
* @param progress the new size
*/
void report(long progress);
}
public interface WriteErrorHandle { public interface WriteErrorHandle {
/** /**
@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream {
class BufferedFile { class BufferedFile {
protected final SharpStream target; final SharpStream target;
private long offset; private long offset;
protected long length; long length;
private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
private int queueSize; private int queueSize;
@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream {
this.target = target; this.target = target;
} }
protected long getOffset() { long getOffset() {
return offset + queueSize;// absolute offset in the file return offset + queueSize;// absolute offset in the file
} }
protected void close() { void close() {
queue = null; queue = null;
target.close(); 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) { while (len > 0) {
// if the queue is full, the method available() will flush the queue // if the queue is full, the method available() will flush the queue
int read = Math.min(available(), len); int read = Math.min(available(), len);
@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream {
target.seek(0); target.seek(0);
} }
protected int available() throws IOException { int available() throws IOException {
if (queueSize >= queue.length) { if (queueSize >= queue.length) {
flush(); flush();
return queue.length; return queue.length;
@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream {
target.seek(0); target.seek(0);
} }
protected void seek(long absoluteOffset) throws IOException { void seek(long absoluteOffset) throws IOException {
if (absoluteOffset == offset) { if (absoluteOffset == offset) {
return;// nothing to do return;// nothing to do
} }

View 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);
}

View File

@ -1,6 +1,6 @@
package us.shandian.giga.postprocessing; 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.OggFromWebMWriter;
import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.io.SharpStream;

View File

@ -1,9 +1,9 @@
package us.shandian.giga.postprocessing; package us.shandian.giga.postprocessing;
import android.os.Message;
import androidx.annotation.NonNull;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File; 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.ChunkFileInputStream;
import us.shandian.giga.io.CircularFileWriter; import us.shandian.giga.io.CircularFileWriter;
import us.shandian.giga.io.CircularFileWriter.OffsetChecker; 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_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_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
public abstract class Postprocessing implements Serializable { 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 * Get a boolean value that indicate if the given algorithm work on the same
* file * file
*/ */
public final boolean worksOnSameFile; public boolean worksOnSameFile;
/** /**
* Indicates whether the selected algorithm needs space reserved at the beginning of the file * 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 * Gets the given algorithm short name
*/ */
private final String name; private String name;
private String[] args; private String[] args;
protected transient DownloadMission mission; private transient DownloadMission mission;
private File tempFile; private File tempFile;
@ -109,16 +109,24 @@ public abstract class Postprocessing implements Serializable {
long finalLength = -1; long finalLength = -1;
mission.done = 0; 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) { if (worksOnSameFile) {
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
try { try {
int i = 0; for (int i = 0, j = 1; i < sources.length; i++, j++) {
for (; i < sources.length - 1; i++) { SharpStream source = mission.storage.getStream();
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); 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)) { if (test(sources)) {
for (SharpStream source : sources) source.rewind(); 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 = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
out.onProgress = this::progressReport; out.onProgress = (long position) -> mission.done = position;
out.onWriteError = (err) -> { out.onWriteError = (err) -> {
mission.psState = 3; mission.psState = 3;
@ -187,11 +195,10 @@ public abstract class Postprocessing implements Serializable {
if (result == OK_RESULT) { if (result == OK_RESULT) {
if (finalLength != -1) { if (finalLength != -1) {
mission.done = finalLength;
mission.length = finalLength; mission.length = finalLength;
} }
} else { } else {
mission.errCode = ERROR_UNKNOWN_EXCEPTION; mission.errCode = ERROR_POSTPROCESSING;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result); mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
} }
@ -229,23 +236,12 @@ public abstract class Postprocessing implements Serializable {
return args[index]; 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 @NonNull
@Override @Override
public String toString() { public String toString() {
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
str.append("name=").append(name).append('['); str.append("{ name=").append(name).append('[');
if (args != null) { if (args != null) {
for (String arg : args) { for (String arg : args) {
@ -255,6 +251,6 @@ public abstract class Postprocessing implements Serializable {
str.delete(0, 1); str.delete(0, 1);
} }
return str.append(']').toString(); return str.append("] }").toString();
} }
} }

View File

@ -2,13 +2,11 @@ package us.shandian.giga.service;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil; 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.File;
import java.io.IOException; import java.io.IOException;
@ -152,6 +150,8 @@ public class DownloadManager {
continue; continue;
} }
mis.threads = new Thread[0];
boolean exists; boolean exists;
try { try {
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
@ -170,8 +170,6 @@ public class DownloadManager {
// is Java IO (avoid showing the "Save as..." dialog) // is Java IO (avoid showing the "Save as..." dialog)
if (exists && mis.storage.isDirect() && !mis.storage.delete()) if (exists && mis.storage.isDirect() && !mis.storage.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
} }
mis.psState = 0; mis.psState = 0;
@ -243,7 +241,6 @@ public class DownloadManager {
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
if (canDownloadInCurrentNetwork() && start) { if (canDownloadInCurrentNetwork() && start) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start(); mission.start();
} }
} }
@ -252,7 +249,6 @@ public class DownloadManager {
public void resumeMission(DownloadMission mission) { public void resumeMission(DownloadMission mission) {
if (!mission.running) { if (!mission.running) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start(); mission.start();
} }
} }
@ -261,7 +257,6 @@ public class DownloadManager {
if (mission.running) { if (mission.running) {
mission.setEnqueued(false); mission.setEnqueued(false);
mission.pause(); mission.pause();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
} }
} }
@ -274,7 +269,6 @@ public class DownloadManager {
mFinishedMissionStore.deleteMission(mission); mFinishedMissionStore.deleteMission(mission);
} }
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
mission.delete(); mission.delete();
} }
} }
@ -291,7 +285,6 @@ public class DownloadManager {
mFinishedMissionStore.deleteMission(mission); mFinishedMissionStore.deleteMission(mission);
} }
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
mission.storage = null; mission.storage = null;
mission.delete(); mission.delete();
} }
@ -374,35 +367,29 @@ public class DownloadManager {
} }
public void pauseAllMissions(boolean force) { public void pauseAllMissions(boolean force) {
boolean flag = false;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue; 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(); mission.pause();
flag = true;
} }
} }
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
} }
public void startAllMissions() { public void startAllMissions() {
boolean flag = false;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (mission.running || mission.isCorrupt()) continue; if (mission.running || mission.isCorrupt()) continue;
flag = true;
mission.start(); mission.start();
} }
} }
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
} }
/** /**
@ -483,28 +470,18 @@ public class DownloadManager {
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
int running = 0;
int paused = 0;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (mission.isCorrupt() || mission.isPsRunning()) continue; if (mission.isCorrupt() || mission.isPsRunning()) continue;
if (mission.running && isMetered) { if (mission.running && isMetered) {
paused++;
mission.pause(); mission.pause();
} else if (!mission.running && !isMetered && mission.enqueued) { } else if (!mission.running && !isMetered && mission.enqueued) {
running++;
mission.start(); mission.start();
if (mPrefQueueLimit) break; if (mPrefQueueLimit) break;
} }
} }
} }
if (running > 0) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
return;
}
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
} }
void updateMaximumAttempts() { 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) { public MissionState checkForExistingMission(StoredFileHelper storage) {
synchronized (this) { synchronized (this) {
DownloadMission pending = getPendingMission(storage); DownloadMission pending = getPendingMission(storage);

View File

@ -25,14 +25,15 @@ import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.os.Parcelable; import android.os.Parcelable;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder; 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.R;
import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.download.DownloadActivity;
@ -41,8 +42,6 @@ import org.schabi.newpipe.player.helper.LockManager;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
@ -58,11 +57,11 @@ public class DownloadManagerService extends Service {
private static final String TAG = "DownloadManagerService"; 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_PAUSED = 1;
public static final int MESSAGE_FINISHED = 2; public static final int MESSAGE_FINISHED = 2;
public static final int MESSAGE_PROGRESS = 3; public static final int MESSAGE_ERROR = 3;
public static final int MESSAGE_ERROR = 4; public static final int MESSAGE_DELETED = 4;
public static final int MESSAGE_DELETED = 5;
private static final int FOREGROUND_NOTIFICATION_ID = 1000; private static final int FOREGROUND_NOTIFICATION_ID = 1000;
private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
@ -217,9 +216,11 @@ public class DownloadManagerService extends Service {
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
); );
} }
return START_NOT_STICKY;
} }
} }
return START_NOT_STICKY;
return START_STICKY;
} }
@Override @Override
@ -250,6 +251,7 @@ public class DownloadManagerService extends Service {
if (icDownloadFailed != null) icDownloadFailed.recycle(); if (icDownloadFailed != null) icDownloadFailed.recycle();
if (icLauncher != null) icLauncher.recycle(); if (icLauncher != null) icLauncher.recycle();
mHandler = null;
mManager.pauseAllMissions(true); mManager.pauseAllMissions(true);
} }
@ -274,6 +276,8 @@ public class DownloadManagerService extends Service {
} }
private boolean handleMessage(@NonNull Message msg) { private boolean handleMessage(@NonNull Message msg) {
if (mHandler == null) return true;
DownloadMission mission = (DownloadMission) msg.obj; DownloadMission mission = (DownloadMission) msg.obj;
switch (msg.what) { switch (msg.what) {
@ -284,7 +288,7 @@ public class DownloadManagerService extends Service {
handleConnectivityState(false); handleConnectivityState(false);
updateForegroundState(mManager.runMissions()); updateForegroundState(mManager.runMissions());
break; break;
case MESSAGE_PROGRESS: case MESSAGE_RUNNING:
updateForegroundState(true); updateForegroundState(true);
break; break;
case MESSAGE_ERROR: case MESSAGE_ERROR:
@ -300,11 +304,8 @@ public class DownloadManagerService extends Service {
if (msg.what != MESSAGE_ERROR) if (msg.what != MESSAGE_ERROR)
mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission)); mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
synchronized (mEchoObservers) { for (Callback observer : mEchoObservers)
for (Callback observer : mEchoObservers) { observer.handleMessage(msg);
observer.handleMessage(msg);
}
}
return true; return true;
} }
@ -523,16 +524,6 @@ public class DownloadManagerService extends Service {
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); 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) { private void manageLock(boolean acquire) {
if (acquire == mLockAcquired) return; if (acquire == mLockAcquired) return;
@ -605,11 +596,11 @@ public class DownloadManagerService extends Service {
} }
public void addMissionEventListener(Callback handler) { public void addMissionEventListener(Callback handler) {
manageObservers(handler, true); mEchoObservers.add(handler);
} }
public void removeMissionEventListener(Callback handler) { public void removeMissionEventListener(Callback handler) {
manageObservers(handler, false); mEchoObservers.remove(handler);
} }
public void clearDownloadNotifications() { public void clearDownloadNotifications() {

View File

@ -10,16 +10,6 @@ import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Message; 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.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
@ -34,6 +24,17 @@ import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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.BuildConfig;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; 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 TAG = "MissionAdapter";
private static final String UNDEFINED_PROGRESS = "--.-%"; private static final String UNDEFINED_PROGRESS = "--.-%";
private static final String DEFAULT_MIME_TYPE = "*/*"; private static final String DEFAULT_MIME_TYPE = "*/*";
private static final String UNDEFINED_ETA = "--:--";
static { static {
@ -103,10 +105,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private View mEmptyMessage; private View mEmptyMessage;
private RecoverHelper mRecover; 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; mContext = context;
mDownloadManager = downloadManager; mDownloadManager = downloadManager;
mDeleter = null;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLayout = R.layout.mission_item; mLayout = R.layout.mission_item;
@ -117,7 +120,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
mIterator = downloadManager.getIterator(); mIterator = downloadManager.getIterator();
mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler);
checkEmptyMessageVisibility(); checkEmptyMessageVisibility();
onResume();
} }
@Override @Override
@ -142,17 +148,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
if (h.item.mission instanceof DownloadMission) { if (h.item.mission instanceof DownloadMission) {
mPendingDownloadsItems.remove(h); mPendingDownloadsItems.remove(h);
if (mPendingDownloadsItems.size() < 1) { if (mPendingDownloadsItems.size() < 1) {
setAutoRefresh(false);
checkMasterButtonsVisibility(); checkMasterButtonsVisibility();
} }
} }
h.popupMenu.dismiss(); h.popupMenu.dismiss();
h.item = null; h.item = null;
h.lastTimeStamp = -1; h.resetSpeedMeasure();
h.lastDone = -1;
h.lastCurrent = -1;
h.state = 0;
} }
@Override @Override
@ -191,7 +193,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
h.size.setText(length); h.size.setText(length);
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
h.lastCurrent = mission.current;
updateProgress(h); updateProgress(h);
mPendingDownloadsItems.add(h); mPendingDownloadsItems.add(h);
} else { } else {
@ -216,20 +217,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private void updateProgress(ViewHolderItem h) { private void updateProgress(ViewHolderItem h) {
if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;
long now = System.currentTimeMillis();
DownloadMission mission = (DownloadMission) h.item.mission; DownloadMission mission = (DownloadMission) h.item.mission;
double done = mission.done;
if (h.lastCurrent != mission.current) { long length = mission.getLength();
h.lastCurrent = mission.current; long now = System.currentTimeMillis();
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;
boolean hasError = mission.errCode != ERROR_NOTHING; boolean hasError = mission.errCode != ERROR_NOTHING;
// hide on error // hide on error
@ -237,19 +228,16 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
// show if length is unknown // show if length is unknown
h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength)); h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength));
float progress; double progress;
if (mission.unknownLength) { if (mission.unknownLength) {
progress = Float.NaN; progress = Double.NaN;
h.progress.setProgress(0f); h.progress.setProgress(0f);
} else { } else {
progress = (float) ((double) mission.done / mission.length); progress = done / length;
if (mission.urls.length > 1 && mission.current < mission.urls.length) {
progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length);
}
} }
if (hasError) { if (hasError) {
h.progress.setProgress(isNotFinite(progress) ? 1f : progress); h.progress.setProgress(isNotFinite(progress) ? 1d : progress);
h.status.setText(R.string.msg_error); h.status.setText(R.string.msg_error);
} else if (isNotFinite(progress)) { } else if (isNotFinite(progress)) {
h.status.setText(UNDEFINED_PROGRESS); h.status.setText(UNDEFINED_PROGRESS);
@ -258,59 +246,78 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
h.progress.setProgress(progress); 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) { if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
state = 0; h.size.setText(sizeStr);
return;
} else if (!mission.running) { } else if (!mission.running) {
state = mission.enqueued ? 1 : 2; state = mission.enqueued ? R.string.queued : R.string.paused;
} else if (mission.isPsRunning()) { } else if (mission.isPsRunning()) {
state = 3; state = R.string.post_processing;
} else if (mission.isRecovering()) {
state = R.string.recovering;
} else { } else {
state = 0; state = 0;
} }
if (state != 0) { if (state != 0) {
// update state without download speed // update state without download speed
if (h.state != state) { h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
String statusStr; h.resetSpeedMeasure();
h.state = state; return;
}
switch (state) { if (h.lastTimestamp < 0) {
case 1: h.size.setText(sizeStr);
statusStr = mContext.getString(R.string.queued); h.lastTimestamp = now;
break; h.lastDone = done;
case 2: return;
statusStr = mContext.getString(R.string.paused); }
break;
case 3:
statusStr = mContext.getString(R.string.post_processing);
break;
default:
statusStr = "?";
break;
}
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")")); long deltaTime = now - h.lastTimestamp;
} else if (deltaDone > 0) { double deltaDone = done - h.lastDone;
h.lastTimeStamp = now;
h.lastDone = mission.done;
}
if (h.lastDone > done) {
h.lastDone = done;
h.size.setText(sizeStr);
return; return;
} }
if (deltaDone > 0 && deltaTime > 0) { if (deltaDone > 0 && deltaTime > 0) {
float speed = (deltaDone * 1000f) / deltaTime; float speed = (float) ((deltaDone * 1000d) / deltaTime);
float averageSpeed = speed;
String speedStr = Utility.formatSpeed(speed); if (h.lastSpeedIdx < 0) {
String sizeStr = Utility.formatBytes(length); 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; if (mission.unknownLength) {
h.lastDone = mission.done; 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; return true;
} }
private ViewHolderItem getViewHolder(Object mission) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (h.item.mission == mission) return h;
}
return null;
}
@Override @Override
public boolean handleMessage(@NonNull Message msg) { public boolean handleMessage(@NonNull Message msg) {
if (mStartButton != null && mPauseButton != null) { if (mStartButton != null && mPauseButton != null) {
@ -396,33 +410,28 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
} }
switch (msg.what) { switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
case DownloadManagerService.MESSAGE_ERROR: case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED: case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_DELETED:
case DownloadManagerService.MESSAGE_PAUSED:
break; break;
default: default:
return false; return false;
} }
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) { ViewHolderItem h = getViewHolder(msg.obj);
setAutoRefresh(true); if (h == null) return false;
return true;
}
for (ViewHolderItem h : mPendingDownloadsItems) { switch (msg.what) {
if (h.item.mission != msg.obj) continue; case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_DELETED:
if (msg.what == DownloadManagerService.MESSAGE_FINISHED) {
// DownloadManager should mark the download as finished // DownloadManager should mark the download as finished
applyChanges(); applyChanges();
return true; return true;
}
updateProgress(h);
return true;
} }
return false; updateProgress(h);
return true;
} }
private void showError(@NonNull DownloadMission mission) { 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; msg = R.string.error_insufficient_storage;
break; break;
case ERROR_UNKNOWN_EXCEPTION: case ERROR_UNKNOWN_EXCEPTION:
showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error); if (mission.errObject != null) {
return; showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
} else {
msg = R.string.msg_error;
break;
}
case ERROR_PROGRESS_LOST: case ERROR_PROGRESS_LOST:
msg = R.string.error_progress_lost; msg = R.string.error_progress_lost;
break; break;
@ -521,7 +535,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
request.append(" ["); request.append(" [");
if (mission.recoveryInfo != null) { if (mission.recoveryInfo != null) {
for (MissionRecoveryInfo recovery : mission.recoveryInfo) for (MissionRecoveryInfo recovery : mission.recoveryInfo)
request.append(" {").append(recovery.toString()).append("} "); request.append(' ')
.append(recovery.toString())
.append(' ');
} }
request.append("]"); request.append("]");
@ -556,16 +572,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
switch (id) { switch (id) {
case R.id.start: case R.id.start:
h.status.setText(UNDEFINED_PROGRESS); h.status.setText(UNDEFINED_PROGRESS);
h.state = -1;
h.size.setText(Utility.formatBytes(mission.getLength()));
mDownloadManager.resumeMission(mission); mDownloadManager.resumeMission(mission);
return true; return true;
case R.id.pause: case R.id.pause:
h.state = -1;
mDownloadManager.pauseMission(mission); mDownloadManager.pauseMission(mission);
updateProgress(h);
h.lastTimeStamp = -1;
h.lastDone = -1;
return true; return true;
case R.id.error_message_view: case R.id.error_message_view:
showError(mission); showError(mission);
@ -598,12 +608,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
shareFile(h.item.mission); shareFile(h.item.mission);
return true; return true;
case R.id.delete: case R.id.delete:
if (mDeleter == null) { mDeleter.append(h.item.mission);
mDownloadManager.deleteMission(h.item.mission);
} else {
mDeleter.append(h.item.mission);
}
applyChanges(); applyChanges();
checkMasterButtonsVisibility();
return true; return true;
case R.id.md5: case R.id.md5:
case R.id.sha1: case R.id.sha1:
@ -639,7 +646,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
mIterator.end(); mIterator.end();
for (ViewHolderItem item : mPendingDownloadsItems) { for (ViewHolderItem item : mPendingDownloadsItems) {
item.lastTimeStamp = -1; item.resetSpeedMeasure();
} }
notifyDataSetChanged(); notifyDataSetChanged();
@ -672,6 +679,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
public void checkMasterButtonsVisibility() { public void checkMasterButtonsVisibility() {
boolean[] state = mIterator.hasValidPendingMissions(); boolean[] state = mIterator.hasValidPendingMissions();
Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]);
setButtonVisible(mPauseButton, state[0]); setButtonVisible(mPauseButton, state[0]);
setButtonVisible(mStartButton, state[1]); setButtonVisible(mStartButton, state[1]);
} }
@ -681,86 +689,57 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
button.setVisible(visible); button.setVisible(visible);
} }
public void ensurePausedMissions() { public void refreshMissionItems() {
for (ViewHolderItem h : mPendingDownloadsItems) { for (ViewHolderItem h : mPendingDownloadsItems) {
if (((DownloadMission) h.item.mission).running) continue; if (((DownloadMission) h.item.mission).running) continue;
updateProgress(h); updateProgress(h);
h.lastTimeStamp = -1; h.resetSpeedMeasure();
h.lastDone = -1;
} }
} }
public void deleterDispose(boolean commitChanges) { public void onDestroy() {
if (mDeleter != null) mDeleter.dispose(commitChanges); mDeleter.dispose();
} }
public void deleterLoad(View view) { public void onResume() {
if (mDeleter == null) mDeleter.resume();
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler); 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() { public void onPaused() {
setAutoRefresh(false); mDeleter.pause();
mHandler.removeCallbacks(rUpdater);
} }
private void setAutoRefresh(boolean enabled) {
if (enabled && !mUpdaterRunning) { public void recoverMission(DownloadMission mission) {
mUpdaterRunning = true; ViewHolderItem h = getViewHolder(mission);
updater(); if (h == null) return;
} else if (!enabled && mUpdaterRunning) {
mUpdaterRunning = false; mission.errObject = null;
mHandler.removeCallbacks(rUpdater); 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() { private void updater() {
if (!mUpdaterRunning) return;
boolean running = false;
for (ViewHolderItem h : mPendingDownloadsItems) { for (ViewHolderItem h : mPendingDownloadsItems) {
// check if the mission is running first // check if the mission is running first
if (!((DownloadMission) h.item.mission).running) continue; if (!((DownloadMission) h.item.mission).running) continue;
updateProgress(h); updateProgress(h);
running = true;
} }
if (running) { mHandler.postDelayed(rUpdater, 1000);
mHandler.postDelayed(rUpdater, 1000);
} else {
mUpdaterRunning = false;
}
} }
private boolean isNotFinite(Float value) { private boolean isNotFinite(double value) {
return Float.isNaN(value) || Float.isInfinite(value); return Double.isNaN(value) || Double.isInfinite(value);
} }
public void setRecover(@NonNull RecoverHelper callback) { public void setRecover(@NonNull RecoverHelper callback) {
@ -789,10 +768,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
MenuItem source; MenuItem source;
MenuItem checksum; MenuItem checksum;
long lastTimeStamp = -1; long lastTimestamp = -1;
long lastDone = -1; double lastDone;
int lastCurrent = -1; int lastSpeedIdx;
int state = 0; float[] lastSpeed = new float[3];
String estimatedTimeArrival = UNDEFINED_ETA;
ViewHolderItem(View view) { ViewHolderItem(View view) {
super(view); super(view);
@ -902,6 +882,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
return popup; return popup;
} }
private void resetSpeedMeasure() {
estimatedTimeArrival = UNDEFINED_ETA;
lastTimestamp = -1;
lastSpeedIdx = -1;
}
} }
class ViewHolderHeader extends RecyclerView.ViewHolder { class ViewHolderHeader extends RecyclerView.ViewHolder {

View File

@ -4,9 +4,10 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.os.Handler; import android.os.Handler;
import com.google.android.material.snackbar.Snackbar;
import android.view.View; import android.view.View;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import java.util.ArrayList; import java.util.ArrayList;
@ -113,7 +114,7 @@ public class Deleter {
show(); show();
} }
private void pause() { public void pause() {
running = false; running = false;
mHandler.removeCallbacks(rNext); mHandler.removeCallbacks(rNext);
mHandler.removeCallbacks(rShow); mHandler.removeCallbacks(rShow);
@ -126,13 +127,11 @@ public class Deleter {
mHandler.postDelayed(rShow, DELAY_RESUME); mHandler.postDelayed(rShow, DELAY_RESUME);
} }
public void dispose(boolean commitChanges) { public void dispose() {
if (items.size() < 1) return; if (items.size() < 1) return;
pause(); pause();
if (!commitChanges) return;
for (Mission mission : items) mDownloadManager.deleteMission(mission); for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null; items = null;
} }

View File

@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable {
mForegroundColor = foreground; mForegroundColor = foreground;
} }
public void setProgress(float progress) { public void setProgress(double progress) {
mProgress = progress; mProgress = (float) progress;
invalidateSelf(); invalidateSelf();
} }

View File

@ -12,11 +12,6 @@ import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager; 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.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -24,6 +19,12 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast; 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 com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -72,8 +73,7 @@ public class MissionsFragment extends Fragment {
mBinder = (DownloadManagerBinder) binder; mBinder = (DownloadManagerBinder) binder;
mBinder.clearDownloadNotifications(); mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView());
mAdapter.deleterLoad(getView());
mAdapter.setRecover(MissionsFragment.this::recoverMission); mAdapter.setRecover(MissionsFragment.this::recoverMission);
@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment {
* Added in API level 23. * Added in API level 23.
*/ */
@Override @Override
public void onAttach(Context context) { public void onAttach(@NonNull Context context) {
super.onAttach(context); super.onAttach(context);
// Bug: in api< 23 this is never called // Bug: in api< 23 this is never called
@ -147,7 +147,7 @@ public class MissionsFragment extends Fragment {
*/ */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @Override
public void onAttach(Activity activity) { public void onAttach(@NonNull Activity activity) {
super.onAttach(activity); super.onAttach(activity);
mContext = activity; mContext = activity;
@ -162,7 +162,7 @@ public class MissionsFragment extends Fragment {
mBinder.removeMissionEventListener(mAdapter); mBinder.removeMissionEventListener(mAdapter);
mBinder.enableNotifications(true); mBinder.enableNotifications(true);
mContext.unbindService(mConnection); mContext.unbindService(mConnection);
mAdapter.deleterDispose(true); mAdapter.onDestroy();
mBinder = null; mBinder = null;
mAdapter = null; mAdapter = null;
@ -196,13 +196,11 @@ public class MissionsFragment extends Fragment {
prompt.create().show(); prompt.create().show();
return true; return true;
case R.id.start_downloads: case R.id.start_downloads:
item.setVisible(false);
mBinder.getDownloadManager().startAllMissions(); mBinder.getDownloadManager().startAllMissions();
return true; return true;
case R.id.pause_downloads: case R.id.pause_downloads:
item.setVisible(false);
mBinder.getDownloadManager().pauseAllMissions(false); mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.ensurePausedMissions();// update items view mAdapter.refreshMissionItems();// update items view
default: default:
return super.onOptionsItemSelected(item); 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 @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (mAdapter != null) { if (mAdapter != null) {
mAdapter.deleterResume(); mAdapter.onResume();
if (mForceUpdate) { if (mForceUpdate) {
mForceUpdate = false; mForceUpdate = false;
@ -303,7 +290,13 @@ public class MissionsFragment extends Fragment {
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (mAdapter != null) mAdapter.onPaused();
if (mAdapter != null) {
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter);
mAdapter.onPaused();
}
if (mBinder != null) mBinder.enableNotifications(true); if (mBinder != null) mBinder.enableNotifications(true);
} }

View File

@ -4,13 +4,14 @@ import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.io.SharpStream;
@ -26,6 +27,7 @@ import java.io.Serializable;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.io.StoredFileHelper;
@ -39,26 +41,28 @@ public class Utility {
} }
public static String formatBytes(long bytes) { public static String formatBytes(long bytes) {
Locale locale = Locale.getDefault();
if (bytes < 1024) { if (bytes < 1024) {
return String.format("%d B", bytes); return String.format(locale, "%d B", bytes);
} else if (bytes < 1024 * 1024) { } 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) { } else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / 1024d / 1024d); return String.format(locale, "%.2f MB", bytes / 1024d / 1024d);
} else { } 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) { if (speed < 1024) {
return String.format("%.2f B/s", speed); return String.format(locale, "%.2f B/s", speed);
} else if (speed < 1024 * 1024) { } 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) { } 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 { } 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) { switch (type) {
case MUSIC: case MUSIC:
return R.drawable.music; return R.drawable.music;
default:
case VIDEO: case VIDEO:
return R.drawable.video; return R.drawable.video;
case SUBTITLE: case SUBTITLE:
return R.drawable.subtitle; return R.drawable.subtitle;
default:
return R.drawable.video;
} }
} }
@ -274,4 +277,25 @@ public class Utility {
return -1; 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);
}
} }

View File

@ -471,7 +471,6 @@
<string name="error_http_not_found">غير موجود</string> <string name="error_http_not_found">غير موجود</string>
<string name="error_postprocessing_failed">فشلت المعالجة الاولية</string> <string name="error_postprocessing_failed">فشلت المعالجة الاولية</string>
<string name="clear_finished_download">حذف التنزيلات المنتهية</string> <string name="clear_finished_download">حذف التنزيلات المنتهية</string>
<string name="msg_pending_downloads">"قم بإستكمال %s حيثما يتم التحويل من التنزيلات"</string>
<string name="stop">توقف</string> <string name="stop">توقف</string>
<string name="max_retry_msg">أقصى عدد للمحاولات</string> <string name="max_retry_msg">أقصى عدد للمحاولات</string>
<string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string> <string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string>

View File

@ -458,7 +458,6 @@
<string name="error_http_not_found">Не знойдзена</string> <string name="error_http_not_found">Не знойдзена</string>
<string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string> <string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string>
<string name="clear_finished_download">Ачысціць завершаныя</string> <string name="clear_finished_download">Ачысціць завершаныя</string>
<string name="msg_pending_downloads">Аднавіць прыпыненыя загрузкі (%s)</string>
<string name="stop">Спыніць</string> <string name="stop">Спыніць</string>
<string name="max_retry_msg">Максімум спробаў</string> <string name="max_retry_msg">Максімум спробаў</string>
<string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string> <string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string>

View File

@ -460,7 +460,6 @@
<string name="app_update_notification_content_title">NewPipe 更新可用!</string> <string name="app_update_notification_content_title">NewPipe 更新可用!</string>
<string name="error_path_creation">无法创建目标文件夹</string> <string name="error_path_creation">无法创建目标文件夹</string>
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试</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="pause_downloads_on_mobile_desc">切换至移动数据时有用,尽管一些下载无法被暂停</string>
<string name="show_comments_title">显示评论</string> <string name="show_comments_title">显示评论</string>
<string name="show_comments_summary">禁用停止显示评论</string> <string name="show_comments_summary">禁用停止显示评论</string>

View File

@ -466,7 +466,6 @@ otevření ve vyskakovacím okně</string>
<string name="error_http_not_found">Nenalezeno</string> <string name="error_http_not_found">Nenalezeno</string>
<string name="error_postprocessing_failed">Post-processing selhal</string> <string name="error_postprocessing_failed">Post-processing selhal</string>
<string name="clear_finished_download">Vyčistit dokončená stahování</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="stop">Zastavit</string>
<string name="max_retry_msg">Maximální počet pokusů o opakování</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> <string name="max_retry_desc">Maximální počet pokusů před zrušením stahování</string>

View File

@ -447,7 +447,6 @@
<string name="paused">sat på pause</string> <string name="paused">sat på pause</string>
<string name="queued">sat i kø</string> <string name="queued">sat i kø</string>
<string name="clear_finished_download">Ryd færdige downloads</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_msg">Maksimalt antal genforsøg</string>
<string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</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> <string name="pause_downloads_on_mobile">Sæt på pause ved skift til mobildata</string>

View File

@ -457,7 +457,6 @@
<string name="error_http_not_found">Nicht gefunden</string> <string name="error_http_not_found">Nicht gefunden</string>
<string name="error_postprocessing_failed">Nachbearbeitung fehlgeschlagen</string> <string name="error_postprocessing_failed">Nachbearbeitung fehlgeschlagen</string>
<string name="clear_finished_download">Um fertige Downloads bereinigen</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="stop">Anhalten</string>
<string name="max_retry_msg">Maximale Wiederholungen</string> <string name="max_retry_msg">Maximale Wiederholungen</string>
<string name="max_retry_desc">Maximalanzahl der Versuche, bevor der Download abgebrochen wird</string> <string name="max_retry_desc">Maximalanzahl der Versuche, bevor der Download abgebrochen wird</string>

View File

@ -459,7 +459,6 @@
<string name="error_http_not_found">Δεν βρέθηκε</string> <string name="error_http_not_found">Δεν βρέθηκε</string>
<string name="error_postprocessing_failed">Μετεπεξεργασία απέτυχε</string> <string name="error_postprocessing_failed">Μετεπεξεργασία απέτυχε</string>
<string name="clear_finished_download">Εκκαθάριση ολοκληρωμένων λήψεων</string> <string name="clear_finished_download">Εκκαθάριση ολοκληρωμένων λήψεων</string>
<string name="msg_pending_downloads">Συνέχιση των %s εκκρεμών σας λήψεων</string>
<string name="stop">Διακοπή</string> <string name="stop">Διακοπή</string>
<string name="max_retry_msg">Μέγιστες επαναπροσπάθειες</string> <string name="max_retry_msg">Μέγιστες επαναπροσπάθειες</string>
<string name="max_retry_desc">Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης</string> <string name="max_retry_desc">Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης</string>

View File

@ -406,6 +406,7 @@
<string name="paused">pausado</string> <string name="paused">pausado</string>
<string name="queued">en cola</string> <string name="queued">en cola</string>
<string name="post_processing">posprocesamiento</string> <string name="post_processing">posprocesamiento</string>
<string name="recovering">recuperando</string>
<string name="enqueue">Añadir a cola</string> <string name="enqueue">Añadir a cola</string>
<string name="permission_denied">Acción denegada por el sistema</string> <string name="permission_denied">Acción denegada por el sistema</string>
<string name="file_deleted">Se eliminó el archivo</string> <string name="file_deleted">Se eliminó el archivo</string>
@ -424,7 +425,6 @@
<string name="grid">Mostrar como grilla</string> <string name="grid">Mostrar como grilla</string>
<string name="list">Mostrar como lista</string> <string name="list">Mostrar como lista</string>
<string name="clear_finished_download">Limpiar descargas finalizadas</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="confirm_prompt">¿Lo confirma\?</string>
<string name="stop">Detener</string> <string name="stop">Detener</string>
<string name="max_retry_msg">Intentos máximos</string> <string name="max_retry_msg">Intentos máximos</string>

View File

@ -460,7 +460,6 @@
<string name="error_http_not_found">Ei leitud</string> <string name="error_http_not_found">Ei leitud</string>
<string name="error_postprocessing_failed">Järeltöötlemine nurjus</string> <string name="error_postprocessing_failed">Järeltöötlemine nurjus</string>
<string name="clear_finished_download">Eemalda lõpetatud allalaadimised</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="stop">Stopp</string>
<string name="max_retry_msg">Korduskatseid</string> <string name="max_retry_msg">Korduskatseid</string>
<string name="max_retry_desc">Suurim katsete arv enne allalaadimise tühistamist</string> <string name="max_retry_desc">Suurim katsete arv enne allalaadimise tühistamist</string>

View File

@ -459,7 +459,6 @@
<string name="error_http_not_found">Ez aurkitua</string> <string name="error_http_not_found">Ez aurkitua</string>
<string name="error_postprocessing_failed">Post-prozesuak huts egin du</string> <string name="error_postprocessing_failed">Post-prozesuak huts egin du</string>
<string name="clear_finished_download">Garbitu amaitutako deskargak</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="stop">Gelditu</string>
<string name="max_retry_msg">Gehienezko saiakerak</string> <string name="max_retry_msg">Gehienezko saiakerak</string>
<string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string> <string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string>

View File

@ -466,7 +466,6 @@
<string name="max_retry_desc">Nombre maximum de tentatives avant dannuler le téléchargement</string> <string name="max_retry_desc">Nombre maximum de tentatives avant dannuler 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="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 naccepte pas les téléchargements multi-fils, veuillez réessayer avec @string/msg_threads = 1</string> <string name="error_http_unsupported_range">Le serveur naccepte 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_title">Afficher les commentaires</string>
<string name="show_comments_summary">Désactiver pour ne pas 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> <string name="autoplay_title">Lecture automatique</string>

View File

@ -464,7 +464,6 @@
<string name="error_http_not_found">לא נמצא</string> <string name="error_http_not_found">לא נמצא</string>
<string name="error_postprocessing_failed">העיבוד המאוחר נכשל</string> <string name="error_postprocessing_failed">העיבוד המאוחר נכשל</string>
<string name="clear_finished_download">פינוי ההורדות שהסתיימו</string> <string name="clear_finished_download">פינוי ההורדות שהסתיימו</string>
<string name="msg_pending_downloads">ניתן להמשיך את %s ההורדות הממתינות שלך דרך ההורדות</string>
<string name="stop">עצירה</string> <string name="stop">עצירה</string>
<string name="max_retry_msg">מספר הניסיונות החוזרים המרבי</string> <string name="max_retry_msg">מספר הניסיונות החוזרים המרבי</string>
<string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string> <string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string>

View File

@ -457,7 +457,6 @@
<string name="error_http_not_found">Nije pronađeno</string> <string name="error_http_not_found">Nije pronađeno</string>
<string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string> <string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string>
<string name="clear_finished_download">Obriši završena preuzimanja</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="stop">Stop</string>
<string name="max_retry_msg">Maksimalnih ponovnih pokušaja</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> <string name="max_retry_desc">Maksimalni broj pokušaja prije poništavanja preuzimanja</string>

View File

@ -453,7 +453,6 @@
<string name="error_http_not_found">Tidak ditemukan</string> <string name="error_http_not_found">Tidak ditemukan</string>
<string name="error_postprocessing_failed">Pengolahan-pasca gagal</string> <string name="error_postprocessing_failed">Pengolahan-pasca gagal</string>
<string name="clear_finished_download">Hapus unduhan yang sudah selesai</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="stop">Berhenti</string>
<string name="max_retry_msg">Percobaan maksimum</string> <string name="max_retry_msg">Percobaan maksimum</string>
<string name="max_retry_desc">Jumlah upaya maksimum sebelum membatalkan unduhan</string> <string name="max_retry_desc">Jumlah upaya maksimum sebelum membatalkan unduhan</string>

View File

@ -457,7 +457,6 @@
<string name="error_http_not_found">Non trovato</string> <string name="error_http_not_found">Non trovato</string>
<string name="error_postprocessing_failed">Post-processing fallito</string> <string name="error_postprocessing_failed">Post-processing fallito</string>
<string name="clear_finished_download">Pulisci i download completati</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="stop">Ferma</string>
<string name="max_retry_msg">Tentativi massimi</string> <string name="max_retry_msg">Tentativi massimi</string>
<string name="max_retry_desc">Tentativi massimi prima di cancellare il download</string> <string name="max_retry_desc">Tentativi massimi prima di cancellare il download</string>

View File

@ -456,7 +456,6 @@
<string name="saved_tabs_invalid_json">デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました</string> <string name="saved_tabs_invalid_json">デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました</string>
<string name="main_page_content_summary">メインページに表示されるタブ</string> <string name="main_page_content_summary">メインページに表示されるタブ</string>
<string name="updates_setting_description">新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します</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">従量制課金ネットワークの割り込み</string>
<string name="pause_downloads_on_mobile_desc">モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません</string> <string name="pause_downloads_on_mobile_desc">モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません</string>
<string name="show_comments_title">コメントを表示</string> <string name="show_comments_title">コメントを表示</string>

View File

@ -454,7 +454,6 @@
<string name="error_http_not_found">HTTP 찾을 수 없습니다</string> <string name="error_http_not_found">HTTP 찾을 수 없습니다</string>
<string name="error_postprocessing_failed">후처리 작업이 실패하였습니다</string> <string name="error_postprocessing_failed">후처리 작업이 실패하였습니다</string>
<string name="clear_finished_download">완료된 다운로드 비우기</string> <string name="clear_finished_download">완료된 다운로드 비우기</string>
<string name="msg_pending_downloads">대기중인 %s 다운로드를 지속하세요</string>
<string name="stop">멈추기</string> <string name="stop">멈추기</string>
<string name="max_retry_msg">최대 재시도 횟수</string> <string name="max_retry_msg">최대 재시도 횟수</string>
<string name="max_retry_desc">다운로드를 취소하기 전까지 다시 시도할 최대 횟수</string> <string name="max_retry_desc">다운로드를 취소하기 전까지 다시 시도할 최대 횟수</string>

View File

@ -453,7 +453,6 @@
<string name="error_http_not_found">Tidak ditemui</string> <string name="error_http_not_found">Tidak ditemui</string>
<string name="error_postprocessing_failed">Pemprosesan-pasca gagal</string> <string name="error_postprocessing_failed">Pemprosesan-pasca gagal</string>
<string name="clear_finished_download">Hapuskan senarai muat turun yang selesai</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="stop">Berhenti</string>
<string name="max_retry_msg">Percubaan maksimum</string> <string name="max_retry_msg">Percubaan maksimum</string>
<string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string> <string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string>

View File

@ -458,7 +458,6 @@
<string name="error_http_not_found">Ikke funnet</string> <string name="error_http_not_found">Ikke funnet</string>
<string name="error_postprocessing_failed">Etterbehandling mislyktes</string> <string name="error_postprocessing_failed">Etterbehandling mislyktes</string>
<string name="clear_finished_download">Tøm fullførte nedlastinger</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="stop">Stopp</string>
<string name="max_retry_msg">Maksimalt antall forsøk</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> <string name="max_retry_desc">Maksimalt antall tilkoblingsforsøk før nedlastingen avblåses</string>

View File

@ -457,7 +457,6 @@
<string name="error_http_not_found">Niet gevonden</string> <string name="error_http_not_found">Niet gevonden</string>
<string name="error_postprocessing_failed">Nabewerking mislukt</string> <string name="error_postprocessing_failed">Nabewerking mislukt</string>
<string name="clear_finished_download">Voltooide downloads wissen</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="stop">Stoppen</string>
<string name="max_retry_msg">Maximaal aantal pogingen</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> <string name="max_retry_desc">Maximaal aantal pogingen vooraleer dat den download wordt geannuleerd</string>

View File

@ -457,7 +457,6 @@
<string name="error_http_not_found">Niet gevonden</string> <string name="error_http_not_found">Niet gevonden</string>
<string name="error_postprocessing_failed">Nabewerking mislukt</string> <string name="error_postprocessing_failed">Nabewerking mislukt</string>
<string name="clear_finished_download">Voltooide downloads wissen</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="stop">Stop</string>
<string name="max_retry_msg">Maximum aantal keer proberen</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> <string name="max_retry_desc">Maximum aantal pogingen voordat de download wordt geannuleerd</string>

View File

@ -453,7 +453,6 @@
<string name="error_http_not_found">ਨਹੀਂ ਲਭਿਆ</string> <string name="error_http_not_found">ਨਹੀਂ ਲਭਿਆ</string>
<string name="error_postprocessing_failed">Post-processing ਫੇਲ੍ਹ</string> <string name="error_postprocessing_failed">Post-processing ਫੇਲ੍ਹ</string>
<string name="clear_finished_download">ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string> <string name="clear_finished_download">ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string>
<string name="msg_pending_downloads">ਡਾਉਨਲੋਡਸ ਤੋਂ ਆਪਣੀਆਂ %s ਬਕਾਇਆ ਟ੍ਰਾਂਸਫਰ ਜਾਰੀ ਰੱਖੋ</string>
<string name="stop">ਰੁੱਕੋ</string> <string name="stop">ਰੁੱਕੋ</string>
<string name="max_retry_msg">ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string> <string name="max_retry_msg">ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string>
<string name="max_retry_desc">ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string> <string name="max_retry_desc">ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string>

View File

@ -459,7 +459,6 @@
<string name="error_http_not_found">Nie znaleziono</string> <string name="error_http_not_found">Nie znaleziono</string>
<string name="error_postprocessing_failed">Przetwarzanie końcowe nie powiodło się</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="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="stop">Zatrzymaj</string>
<string name="max_retry_msg">Maksymalna liczba powtórzeń</string> <string name="max_retry_msg">Maksymalna liczba powtórzeń</string>
<string name="max_retry_desc">Maksymalna liczba prób przed anulowaniem pobierania</string> <string name="max_retry_desc">Maksymalna liczba prób przed anulowaniem pobierania</string>

View File

@ -466,7 +466,6 @@ abrir em modo popup</string>
<string name="error_http_not_found">Não encontrado</string> <string name="error_http_not_found">Não encontrado</string>
<string name="error_postprocessing_failed">Falha no pós processamento</string> <string name="error_postprocessing_failed">Falha no pós processamento</string>
<string name="clear_finished_download">Limpar downloads finalizados</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="stop">Parar</string>
<string name="max_retry_msg">Tentativas Máximas</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> <string name="max_retry_desc">Número máximo de tentativas antes de cancelar o download</string>

View File

@ -455,7 +455,6 @@
<string name="error_http_not_found">Não encontrado</string> <string name="error_http_not_found">Não encontrado</string>
<string name="error_postprocessing_failed">Pós-processamento falhado</string> <string name="error_postprocessing_failed">Pós-processamento falhado</string>
<string name="clear_finished_download">Limpar transferências concluídas</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="stop">Parar</string>
<string name="max_retry_msg">Tentativas máximas</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> <string name="max_retry_desc">Número máximo de tentativas antes de cancelar a transferência</string>

View File

@ -464,7 +464,6 @@
<string name="download_finished">Загрузка завершена</string> <string name="download_finished">Загрузка завершена</string>
<string name="download_finished_more">%s загрузок завершено</string> <string name="download_finished_more">%s загрузок завершено</string>
<string name="generate_unique_name">Создать уникальное имя</string> <string name="generate_unique_name">Создать уникальное имя</string>
<string name="msg_pending_downloads">Возобновить приостановленные загрузки (%s)</string>
<string name="max_retry_msg">Максимум попыток</string> <string name="max_retry_msg">Максимум попыток</string>
<string name="max_retry_desc">Количество попыток перед отменой загрузки</string> <string name="max_retry_desc">Количество попыток перед отменой загрузки</string>
<string name="pause_downloads_on_mobile_desc">Некоторые загрузки не поддерживают докачку и начнутся с начала</string> <string name="pause_downloads_on_mobile_desc">Некоторые загрузки не поддерживают докачку и начнутся с начала</string>

View File

@ -465,7 +465,6 @@
<string name="error_http_not_found">Nenájdené</string> <string name="error_http_not_found">Nenájdené</string>
<string name="error_postprocessing_failed">Post-spracovanie zlyhalo</string> <string name="error_postprocessing_failed">Post-spracovanie zlyhalo</string>
<string name="clear_finished_download">Vyčistiť dokončené sťahovania</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="stop">Stop</string>
<string name="max_retry_msg">Maximum opakovaní</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> <string name="max_retry_desc">Maximálny počet pokusov pred zrušením stiahnutia</string>

View File

@ -452,7 +452,6 @@
<string name="error_http_not_found">Bulunamadı</string> <string name="error_http_not_found">Bulunamadı</string>
<string name="error_postprocessing_failed">İşlem sonrası başarısız</string> <string name="error_postprocessing_failed">İşlem sonrası başarısız</string>
<string name="clear_finished_download">Tamamlanan indirmeleri temizle</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="stop">Durdur</string>
<string name="max_retry_msg">Azami deneme sayısı</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> <string name="max_retry_desc">İndirmeyi iptal etmeden önce maksimum deneme sayısı</string>

View File

@ -471,7 +471,6 @@
<string name="saved_tabs_invalid_json">Помилка зчитування збережених вкладок. Використовую типові вкладки.</string> <string name="saved_tabs_invalid_json">Помилка зчитування збережених вкладок. Використовую типові вкладки.</string>
<string name="main_page_content_summary">Вкладки, що відображаються на головній сторінці</string> <string name="main_page_content_summary">Вкладки, що відображаються на головній сторінці</string>
<string name="updates_setting_description">Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії</string> <string name="updates_setting_description">Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії</string>
<string name="msg_pending_downloads">Продовжити ваші %s відкладених переміщень із Завантажень</string>
<string name="pause_downloads_on_mobile_desc">Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені</string> <string name="pause_downloads_on_mobile_desc">Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені</string>
<string name="show_comments_title">Показувати коментарі</string> <string name="show_comments_title">Показувати коментарі</string>
<string name="show_comments_summary">Вимнути відображення дописів</string> <string name="show_comments_summary">Вимнути відображення дописів</string>

View File

@ -452,7 +452,6 @@
<string name="error_http_not_found">Không tìm thấy</string> <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="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="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="stop">Dừng</string>
<string name="max_retry_msg">Số lượt thử lại tối đa</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> <string name="max_retry_desc">Số lượt thử lại trước khi hủy tải về</string>

View File

@ -450,7 +450,6 @@
<string name="error_http_not_found">找不到</string> <string name="error_http_not_found">找不到</string>
<string name="error_postprocessing_failed">後處理失敗</string> <string name="error_postprocessing_failed">後處理失敗</string>
<string name="clear_finished_download">清除已結束的下載</string> <string name="clear_finished_download">清除已結束的下載</string>
<string name="msg_pending_downloads">繼續從您所擱置中的下載 %s 傳輸</string>
<string name="stop">停止</string> <string name="stop">停止</string>
<string name="max_retry_msg">最大重試次數</string> <string name="max_retry_msg">最大重試次數</string>
<string name="max_retry_desc">在取消下載前的最大嘗試數</string> <string name="max_retry_desc">在取消下載前的最大嘗試數</string>

View File

@ -526,6 +526,7 @@
<string name="paused">paused</string> <string name="paused">paused</string>
<string name="queued">queued</string> <string name="queued">queued</string>
<string name="post_processing">post-processing</string> <string name="post_processing">post-processing</string>
<string name="recovering">recovering</string>
<string name="enqueue">Queue</string> <string name="enqueue">Queue</string>
<string name="permission_denied">Action denied by the system</string> <string name="permission_denied">Action denied by the system</string>
<!-- download notifications --> <!-- download notifications -->
@ -560,7 +561,6 @@
<string name="error_download_resource_gone">Cannot recover this download</string> <string name="error_download_resource_gone">Cannot recover this download</string>
<string name="clear_finished_download">Clear finished downloads</string> <string name="clear_finished_download">Clear finished downloads</string>
<string name="confirm_prompt">Are you sure?</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="stop">Stop</string>
<string name="max_retry_msg">Maximum retries</string> <string name="max_retry_msg">Maximum retries</string>
<string name="max_retry_desc">Maximum number of attempts before canceling the download</string> <string name="max_retry_desc">Maximum number of attempts before canceling the download</string>