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 3641f57294
commit ab896a79e1
53 changed files with 554 additions and 622 deletions

View file

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

View file

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

View file

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

View file

@ -87,6 +87,7 @@ public class DownloadRunnable extends Thread {
if (mConn.getResponseCode() == 416) {
if (block.done > 0) {
// try again from the start (of the block)
mMission.notifyProgress(-block.done);
block.done = 0;
retry = true;
mConn.disconnect();
@ -114,7 +115,7 @@ public class DownloadRunnable extends Thread {
int len;
// use always start <= end
// fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly
// fixes a deadlock because in some videos, youtube is sending one byte alone
while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
f.write(buf, 0, len);
start += len;
@ -135,7 +136,7 @@ public class DownloadRunnable extends Thread {
if (mId == 1) {
// only the first thread will execute the recovery procedure
mMission.doRecover(e);
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
}
return;
}

View file

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

View file

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

View file

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