fast download pausing
* fast download pausing * fix UI thread blocking when calling pause() * check running threads before start the download * fix null pointer exception in onDestroy in the download service, without calling onCreate method (android 8)
This commit is contained in:
parent
9f4a7e664f
commit
e2aa36d083
5 changed files with 241 additions and 118 deletions
|
@ -5,6 +5,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.ClosedByInterruptException;
|
||||||
|
@ -13,14 +14,16 @@ import us.shandian.giga.util.Utility;
|
||||||
|
|
||||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
|
|
||||||
public class DownloadInitializer implements Runnable {
|
public class DownloadInitializer extends Thread {
|
||||||
private final static String TAG = "DownloadInitializer";
|
private final static String TAG = "DownloadInitializer";
|
||||||
final static int mId = 0;
|
final static int mId = 0;
|
||||||
|
|
||||||
private DownloadMission mMission;
|
private DownloadMission mMission;
|
||||||
|
private HttpURLConnection mConn;
|
||||||
|
|
||||||
DownloadInitializer(@NonNull DownloadMission mission) {
|
DownloadInitializer(@NonNull DownloadMission mission) {
|
||||||
mMission = mission;
|
mMission = mission;
|
||||||
|
mConn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -32,10 +35,12 @@ public class DownloadInitializer implements Runnable {
|
||||||
try {
|
try {
|
||||||
mMission.currentThreadCount = mMission.threadCount;
|
mMission.currentThreadCount = mMission.threadCount;
|
||||||
|
|
||||||
HttpURLConnection conn = mMission.openConnection(mId, -1, -1);
|
mConn = mMission.openConnection(mId, -1, -1);
|
||||||
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
|
|
||||||
mMission.length = Utility.getContentLength(conn);
|
mMission.length = Utility.getContentLength(mConn);
|
||||||
|
|
||||||
|
|
||||||
if (mMission.length == 0) {
|
if (mMission.length == 0) {
|
||||||
|
@ -44,7 +49,7 @@ public class DownloadInitializer implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for dynamic generated content
|
// check for dynamic generated content
|
||||||
if (mMission.length == -1 && conn.getResponseCode() == 200) {
|
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
|
||||||
mMission.blocks = 0;
|
mMission.blocks = 0;
|
||||||
mMission.length = 0;
|
mMission.length = 0;
|
||||||
mMission.fallback = true;
|
mMission.fallback = true;
|
||||||
|
@ -56,50 +61,54 @@ public class DownloadInitializer implements Runnable {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Open again
|
// Open again
|
||||||
conn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
||||||
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
|
|
||||||
if (code == 206) {
|
synchronized (mMission.blockState) {
|
||||||
if (mMission.currentThreadCount > 1) {
|
if (mConn.getResponseCode() == 206) {
|
||||||
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
|
if (mMission.currentThreadCount > 1) {
|
||||||
|
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
|
||||||
|
|
||||||
if (mMission.currentThreadCount > mMission.blocks) {
|
if (mMission.currentThreadCount > mMission.blocks) {
|
||||||
mMission.currentThreadCount = (int) mMission.blocks;
|
mMission.currentThreadCount = (int) mMission.blocks;
|
||||||
|
}
|
||||||
|
if (mMission.currentThreadCount <= 0) {
|
||||||
|
mMission.currentThreadCount = 1;
|
||||||
|
}
|
||||||
|
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
|
||||||
|
mMission.blocks++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if one thread is solicited don't calculate blocks, is useless
|
||||||
|
mMission.blocks = 1;
|
||||||
|
mMission.fallback = true;
|
||||||
|
mMission.unknownLength = false;
|
||||||
}
|
}
|
||||||
if (mMission.currentThreadCount <= 0) {
|
|
||||||
mMission.currentThreadCount = 1;
|
if (DEBUG) {
|
||||||
}
|
Log.d(TAG, "http response code = " + mConn.getResponseCode());
|
||||||
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
|
|
||||||
mMission.blocks++;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if one thread is solicited don't calculate blocks, is useless
|
// Fallback to single thread
|
||||||
mMission.blocks = 1;
|
mMission.blocks = 0;
|
||||||
mMission.fallback = true;
|
mMission.fallback = true;
|
||||||
mMission.unknownLength = false;
|
mMission.unknownLength = false;
|
||||||
|
mMission.currentThreadCount = 1;
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
for (long i = 0; i < mMission.currentThreadCount; i++) {
|
||||||
Log.d(TAG, "http response code = " + code);
|
mMission.threadBlockPositions.add(i);
|
||||||
}
|
mMission.threadBytePositions.add(0L);
|
||||||
} else {
|
|
||||||
// Fallback to single thread
|
|
||||||
mMission.blocks = 0;
|
|
||||||
mMission.fallback = true;
|
|
||||||
mMission.unknownLength = false;
|
|
||||||
mMission.currentThreadCount = 1;
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "falling back due http response code = " + code);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (long i = 0; i < mMission.currentThreadCount; i++) {
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
mMission.threadBlockPositions.add(i);
|
|
||||||
mMission.threadBytePositions.add(0L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
File file;
|
File file;
|
||||||
|
@ -112,7 +121,7 @@ public class DownloadInitializer implements Runnable {
|
||||||
|
|
||||||
file = new File(file, mMission.name);
|
file = new File(file, mMission.name);
|
||||||
|
|
||||||
// if the name is used by "something", delete it
|
// if the name is used by another process, delete it
|
||||||
if (file.exists() && !file.isFile() && !file.delete()) {
|
if (file.exists() && !file.isFile() && !file.delete()) {
|
||||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
||||||
return;
|
return;
|
||||||
|
@ -131,14 +140,16 @@ public class DownloadInitializer implements Runnable {
|
||||||
af.seek(mMission.offsets[mMission.current]);
|
af.seek(mMission.offsets[mMission.current]);
|
||||||
af.close();
|
af.close();
|
||||||
|
|
||||||
if (Thread.interrupted()) return;
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
|
|
||||||
mMission.running = false;
|
mMission.running = false;
|
||||||
break;
|
break;
|
||||||
|
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||||
|
return;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (e instanceof ClosedByInterruptException) {
|
if (!mMission.running) return;
|
||||||
return;
|
|
||||||
} else if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
||||||
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -150,11 +161,26 @@ public class DownloadInitializer implements Runnable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//try again
|
|
||||||
Log.e(TAG, "initializer failed, retrying", e);
|
Log.e(TAG, "initializer failed, retrying", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hide marquee in the progress bar
|
||||||
|
mMission.done++;
|
||||||
|
|
||||||
mMission.start();
|
mMission.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void interrupt() {
|
||||||
|
super.interrupt();
|
||||||
|
|
||||||
|
if (mConn != null) {
|
||||||
|
try {
|
||||||
|
mConn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,13 +122,13 @@ public class DownloadMission extends Mission {
|
||||||
private transient boolean mWritingToFile;
|
private transient boolean mWritingToFile;
|
||||||
|
|
||||||
@SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable
|
@SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable
|
||||||
private final HashMap<Long, Boolean> blockState = new HashMap<>();
|
final HashMap<Long, Boolean> blockState = new HashMap<>();
|
||||||
final List<Long> threadBlockPositions = new ArrayList<>();
|
final List<Long> threadBlockPositions = new ArrayList<>();
|
||||||
final List<Long> threadBytePositions = new ArrayList<>();
|
final List<Long> threadBytePositions = new ArrayList<>();
|
||||||
|
|
||||||
private transient boolean deleted;
|
private transient boolean deleted;
|
||||||
int currentThreadCount;
|
int currentThreadCount;
|
||||||
private transient Thread[] threads = null;
|
private transient Thread[] threads = new Thread[0];
|
||||||
private transient Thread init = null;
|
private transient Thread init = null;
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,9 +238,8 @@ public class DownloadMission extends Mission {
|
||||||
* @param rangeEnd range end
|
* @param rangeEnd range end
|
||||||
* @return a {@link java.net.URLConnection URLConnection} linking to the URL.
|
* @return a {@link java.net.URLConnection URLConnection} linking to the URL.
|
||||||
* @throws IOException if an I/O exception occurs.
|
* @throws IOException if an I/O exception occurs.
|
||||||
* @throws HttpError if the the http response is not satisfiable
|
|
||||||
*/
|
*/
|
||||||
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException, HttpError {
|
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||||
URL url = new URL(urls[current]);
|
URL url = new URL(urls[current]);
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setInstanceFollowRedirects(true);
|
conn.setInstanceFollowRedirects(true);
|
||||||
|
@ -250,29 +249,45 @@ public class DownloadMission extends Mission {
|
||||||
if (rangeEnd > 0) req += rangeEnd;
|
if (rangeEnd > 0) req += rangeEnd;
|
||||||
|
|
||||||
conn.setRequestProperty("Range", req);
|
conn.setRequestProperty("Range", req);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range"));
|
Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range"));
|
||||||
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.connect();
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param threadId id of the calling thread
|
||||||
|
* @param conn Opens and establish the communication
|
||||||
|
* @throws IOException if an error occurred connecting to the server.
|
||||||
|
* @throws HttpError if the HTTP Status-Code is not satisfiable
|
||||||
|
*/
|
||||||
|
void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError {
|
||||||
|
conn.connect();
|
||||||
int statusCode = conn.getResponseCode();
|
int statusCode = conn.getResponseCode();
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
switch (statusCode) {
|
switch (statusCode) {
|
||||||
case 204:
|
case 204:
|
||||||
case 205:
|
case 205:
|
||||||
case 207:
|
case 207:
|
||||||
throw new HttpError(conn.getResponseCode());
|
throw new HttpError(conn.getResponseCode());
|
||||||
|
case 416:
|
||||||
|
return;// let the download thread handle this error
|
||||||
default:
|
default:
|
||||||
if (statusCode < 200 || statusCode > 299) {
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
throw new HttpError(statusCode);
|
throw new HttpError(statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void notify(int what) {
|
private void notify(int what) {
|
||||||
Message m = new Message();
|
Message m = new Message();
|
||||||
m.what = what;
|
m.what = what;
|
||||||
|
@ -389,6 +404,11 @@ public class DownloadMission extends Mission {
|
||||||
*/
|
*/
|
||||||
public void start() {
|
public void start() {
|
||||||
if (running || current >= urls.length) return;
|
if (running || current >= urls.length) return;
|
||||||
|
|
||||||
|
// ensure that the previous state is completely paused.
|
||||||
|
joinForThread(init);
|
||||||
|
for (Thread thread : threads) joinForThread(thread);
|
||||||
|
|
||||||
enqueued = false;
|
enqueued = false;
|
||||||
running = true;
|
running = true;
|
||||||
errCode = ERROR_NOTHING;
|
errCode = ERROR_NOTHING;
|
||||||
|
@ -400,7 +420,7 @@ public class DownloadMission extends Mission {
|
||||||
|
|
||||||
init = null;
|
init = null;
|
||||||
|
|
||||||
if (threads == null) {
|
if (threads.length < 1) {
|
||||||
threads = new Thread[currentThreadCount];
|
threads = new Thread[currentThreadCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,39 +448,37 @@ public class DownloadMission extends Mission {
|
||||||
recovered = true;
|
recovered = true;
|
||||||
enqueued = false;
|
enqueued = false;
|
||||||
|
|
||||||
if (init != null && init != Thread.currentThread() && init.isAlive()) {
|
if (postprocessingRunning) {
|
||||||
init.interrupt();
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "pause during post-processing is not applicable.");
|
||||||
try {
|
|
||||||
init.join();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
resetState();
|
if (init != null && init.isAlive()) {
|
||||||
|
init.interrupt();
|
||||||
|
synchronized (blockState) {
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG && blocks == 0) {
|
if (DEBUG && blocks == 0) {
|
||||||
Log.w(TAG, "pausing a download that can not be resumed.");
|
Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server).");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (threads == null || Thread.interrupted()) {
|
if (threads == null || Thread.currentThread().isInterrupted()) {
|
||||||
writeThisToFile();
|
writeThisToFile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postprocessingRunning) return;
|
|
||||||
|
|
||||||
// wait for all threads are suspended before save the state
|
// wait for all threads are suspended before save the state
|
||||||
runAsync(-1, () -> {
|
runAsync(-1, () -> {
|
||||||
try {
|
try {
|
||||||
for (Thread thread : threads) {
|
for (Thread thread : threads) {
|
||||||
if (thread == Thread.currentThread()) continue;
|
|
||||||
|
|
||||||
if (thread.isAlive()) {
|
if (thread.isAlive()) {
|
||||||
thread.interrupt();
|
thread.interrupt();
|
||||||
thread.join();
|
thread.join(5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -492,7 +510,7 @@ public class DownloadMission extends Mission {
|
||||||
threadBlockPositions.clear();
|
threadBlockPositions.clear();
|
||||||
threadBytePositions.clear();
|
threadBytePositions.clear();
|
||||||
blockState.clear();
|
blockState.clear();
|
||||||
threads = null;
|
threads = new Thread[0];
|
||||||
|
|
||||||
Utility.writeToFile(metadata, DownloadMission.this);
|
Utility.writeToFile(metadata, DownloadMission.this);
|
||||||
}
|
}
|
||||||
|
@ -571,28 +589,61 @@ public class DownloadMission extends Mission {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* run a method in a new thread
|
* run a new thread
|
||||||
*
|
*
|
||||||
* @param id id of new thread (used for debugging only)
|
* @param id id of new thread (used for debugging only)
|
||||||
* @param who the object whose {@code run} method is invoked when this thread is started
|
* @param who the Runnable whose {@code run} method is invoked.
|
||||||
* @return the created thread
|
|
||||||
*/
|
*/
|
||||||
private Thread runAsync(int id, Runnable who) {
|
private void runAsync(int id, Runnable who) {
|
||||||
|
runAsync(id, new Thread(who));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run a new thread
|
||||||
|
*
|
||||||
|
* @param id id of new thread (used for debugging only)
|
||||||
|
* @param who the Thread whose {@code run} method is invoked when this thread is started
|
||||||
|
* @return the passed thread
|
||||||
|
*/
|
||||||
|
private Thread runAsync(int id, Thread who) {
|
||||||
// known thread ids:
|
// known thread ids:
|
||||||
// -2: state saving by notifyProgress() method
|
// -2: state saving by notifyProgress() method
|
||||||
// -1: wait for saving the state by pause() method
|
// -1: wait for saving the state by pause() method
|
||||||
// 0: initializer
|
// 0: initializer
|
||||||
// >=1: any download thread
|
// >=1: any download thread
|
||||||
|
|
||||||
Thread thread = new Thread(who);
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
thread.setName(String.format("[%s] id = %s filename = %s", TAG, id, name));
|
who.setName(String.format("%s[%s] %s", TAG, id, name));
|
||||||
}
|
}
|
||||||
thread.start();
|
|
||||||
|
|
||||||
return thread;
|
who.start();
|
||||||
|
|
||||||
|
return who;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void joinForThread(Thread thread) {
|
||||||
|
if (thread == null || !thread.isAlive()) return;
|
||||||
|
if (thread == Thread.currentThread()) return;
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "a thread is !still alive!: " + thread.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// still alive, this should not happen.
|
||||||
|
// Possible reasons:
|
||||||
|
// slow device
|
||||||
|
// the user is spamming start/pause buttons
|
||||||
|
// start() method called quickly after pause()
|
||||||
|
|
||||||
|
try {
|
||||||
|
thread.join(10000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.d(TAG, "timeout on join : " + thread.getName());
|
||||||
|
throw new RuntimeException("A thread is still running:\n" + thread.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static class HttpError extends Exception {
|
static class HttpError extends Exception {
|
||||||
int statusCode;
|
int statusCode;
|
||||||
|
|
||||||
|
@ -602,7 +653,7 @@ public class DownloadMission extends Mission {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return "Http status code: " + String.valueOf(statusCode);
|
return "HTTP " + String.valueOf(statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,16 +14,19 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
* Runnable to download blocks of a file until the file is completely downloaded,
|
* Runnable to download blocks of a file until the file is completely downloaded,
|
||||||
* an error occurs or the process is stopped.
|
* an error occurs or the process is stopped.
|
||||||
*/
|
*/
|
||||||
public class DownloadRunnable implements Runnable {
|
public class DownloadRunnable extends Thread {
|
||||||
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
||||||
|
|
||||||
private final DownloadMission mMission;
|
private final DownloadMission mMission;
|
||||||
private final int mId;
|
private final int mId;
|
||||||
|
|
||||||
|
private HttpURLConnection mConn;
|
||||||
|
|
||||||
DownloadRunnable(DownloadMission mission, int id) {
|
DownloadRunnable(DownloadMission mission, int id) {
|
||||||
if (mission == null) throw new NullPointerException("mission is null");
|
if (mission == null) throw new NullPointerException("mission is null");
|
||||||
mMission = mission;
|
mMission = mission;
|
||||||
mId = id;
|
mId = id;
|
||||||
|
mConn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -47,12 +50,7 @@ public class DownloadRunnable implements Runnable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running && blockPosition < mMission.blocks) {
|
while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING && blockPosition < mMission.blocks) {
|
||||||
|
|
||||||
if (Thread.currentThread().isInterrupted()) {
|
|
||||||
mMission.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG && retry) {
|
if (DEBUG && retry) {
|
||||||
Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition);
|
Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition);
|
||||||
|
@ -83,8 +81,9 @@ public class DownloadRunnable implements Runnable {
|
||||||
|
|
||||||
long start = blockPosition * DownloadMission.BLOCK_SIZE;
|
long start = blockPosition * DownloadMission.BLOCK_SIZE;
|
||||||
long end = start + DownloadMission.BLOCK_SIZE - 1;
|
long end = start + DownloadMission.BLOCK_SIZE - 1;
|
||||||
|
long offset = mMission.getThreadBytePosition(mId);
|
||||||
|
|
||||||
start += mMission.getThreadBytePosition(mId);
|
start += offset;
|
||||||
|
|
||||||
if (end >= mMission.length) {
|
if (end >= mMission.length) {
|
||||||
end = mMission.length - 1;
|
end = mMission.length - 1;
|
||||||
|
@ -93,14 +92,21 @@ public class DownloadRunnable implements Runnable {
|
||||||
long total = 0;
|
long total = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpURLConnection conn = mMission.openConnection(mId, start, end);
|
mConn = mMission.openConnection(mId, start, end);
|
||||||
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
|
// check if the download can be resumed
|
||||||
|
if (mConn.getResponseCode() == 416 && offset > 0) {
|
||||||
|
retryCount--;
|
||||||
|
throw new DownloadMission.HttpError(416);
|
||||||
|
}
|
||||||
|
|
||||||
// The server may be ignoring the range request
|
// The server may be ignoring the range request
|
||||||
if (conn.getResponseCode() != 206) {
|
if (mConn.getResponseCode() != 206) {
|
||||||
mMission.notifyError(new DownloadMission.HttpError(conn.getResponseCode()));
|
mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode()));
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode());
|
Log.e(TAG, mId + ":Unsupported " + mConn.getResponseCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -108,7 +114,8 @@ public class DownloadRunnable implements Runnable {
|
||||||
|
|
||||||
f.seek(mMission.offsets[mMission.current] + start);
|
f.seek(mMission.offsets[mMission.current] + start);
|
||||||
|
|
||||||
is = conn.getInputStream();
|
is = mConn.getInputStream();
|
||||||
|
|
||||||
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
|
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
|
||||||
int len;
|
int len;
|
||||||
|
|
||||||
|
@ -121,18 +128,17 @@ public class DownloadRunnable implements Runnable {
|
||||||
|
|
||||||
if (DEBUG && mMission.running) {
|
if (DEBUG && mMission.running) {
|
||||||
Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded");
|
Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded");
|
||||||
mMission.setThreadBytePosition(mId, 0L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the download is paused, save progress for this thread
|
if (mMission.running)
|
||||||
if (!mMission.running) {
|
mMission.setThreadBytePosition(mId, 0L);// clear byte position for next block
|
||||||
mMission.setThreadBytePosition(mId, total);
|
else
|
||||||
break;
|
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
mMission.setThreadBytePosition(mId, total);
|
mMission.setThreadBytePosition(mId, total);
|
||||||
|
|
||||||
if (e instanceof ClosedByInterruptException) break;
|
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||||
|
|
||||||
if (retryCount++ >= mMission.maxRetry) {
|
if (retryCount++ >= mMission.maxRetry) {
|
||||||
mMission.notifyError(e);
|
mMission.notifyError(e);
|
||||||
|
@ -147,29 +153,43 @@ public class DownloadRunnable implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
f.close();
|
|
||||||
} catch (Exception err) {
|
|
||||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (is != null) is.close();
|
if (is != null) is.close();
|
||||||
} catch (Exception err) {
|
} catch (Exception err) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
f.close();
|
||||||
|
} catch (Exception err) {
|
||||||
|
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||||
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "thread " + mId + " exited from main download loop");
|
Log.d(TAG, "thread " + mId + " exited from main download loop");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) {
|
if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "no error has happened, notifying");
|
Log.d(TAG, "no error has happened, notifying");
|
||||||
}
|
}
|
||||||
mMission.notifyFinished();
|
mMission.notifyFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG && !mMission.running) {
|
if (DEBUG && !mMission.running) {
|
||||||
Log.d(TAG, "The mission has been paused. Passing.");
|
Log.d(TAG, "The mission has been paused. Passing.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void interrupt() {
|
||||||
|
super.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mConn != null) mConn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,30 +18,33 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
/**
|
/**
|
||||||
* Single-threaded fallback mode
|
* Single-threaded fallback mode
|
||||||
*/
|
*/
|
||||||
public class DownloadRunnableFallback implements Runnable {
|
public class DownloadRunnableFallback extends Thread {
|
||||||
private static final String TAG = "DownloadRunnableFallback";
|
private static final String TAG = "DownloadRunnableFallback";
|
||||||
|
|
||||||
private final DownloadMission mMission;
|
private final DownloadMission mMission;
|
||||||
private int retryCount = 0;
|
private final int mId = 1;
|
||||||
|
|
||||||
private InputStream is;
|
private int mRetryCount = 0;
|
||||||
private RandomAccessFile f;
|
private InputStream mIs;
|
||||||
|
private RandomAccessFile mF;
|
||||||
|
private HttpURLConnection mConn;
|
||||||
|
|
||||||
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
||||||
mMission = mission;
|
mMission = mission;
|
||||||
is = null;
|
mIs = null;
|
||||||
f = null;
|
mF = null;
|
||||||
|
mConn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dispose() {
|
private void dispose() {
|
||||||
try {
|
try {
|
||||||
if (is != null) is.close();
|
if (mIs != null) mIs.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (f != null) f.close();
|
if (mF != null) mF.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||||
}
|
}
|
||||||
|
@ -63,27 +66,36 @@ public class DownloadRunnableFallback implements Runnable {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||||
HttpURLConnection conn = mMission.openConnection(1, rangeStart, -1);
|
|
||||||
|
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||||
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
|
// check if the download can be resumed
|
||||||
|
if (mConn.getResponseCode() == 416 && start > 0) {
|
||||||
|
start = 0;
|
||||||
|
mRetryCount--;
|
||||||
|
throw new DownloadMission.HttpError(416);
|
||||||
|
}
|
||||||
|
|
||||||
// secondary check for the file length
|
// secondary check for the file length
|
||||||
if (!mMission.unknownLength)
|
if (!mMission.unknownLength)
|
||||||
mMission.unknownLength = Utility.getContentLength(conn) == -1;
|
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||||
|
|
||||||
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||||
f.seek(mMission.offsets[mMission.current] + start);
|
mF.seek(mMission.offsets[mMission.current] + start);
|
||||||
|
|
||||||
is = conn.getInputStream();
|
mIs = mConn.getInputStream();
|
||||||
|
|
||||||
byte[] buf = new byte[64 * 1024];
|
byte[] buf = new byte[64 * 1024];
|
||||||
int len = 0;
|
int len = 0;
|
||||||
|
|
||||||
while (mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
|
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
|
||||||
f.write(buf, 0, len);
|
mF.write(buf, 0, len);
|
||||||
start += len;
|
start += len;
|
||||||
mMission.notifyProgress(len);
|
mMission.notifyProgress(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if thread goes interrupted check if the last part is written. This avoid re-download the whole file
|
// if thread goes interrupted check if the last part mIs written. This avoid re-download the whole file
|
||||||
done = len == -1;
|
done = len == -1;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
dispose();
|
dispose();
|
||||||
|
@ -91,9 +103,9 @@ public class DownloadRunnableFallback implements Runnable {
|
||||||
// save position
|
// save position
|
||||||
mMission.setThreadBytePosition(0, start);
|
mMission.setThreadBytePosition(0, start);
|
||||||
|
|
||||||
if (e instanceof ClosedByInterruptException) return;
|
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||||
|
|
||||||
if (retryCount++ >= mMission.maxRetry) {
|
if (mRetryCount++ >= mMission.maxRetry) {
|
||||||
mMission.notifyError(e);
|
mMission.notifyError(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -110,4 +122,18 @@ public class DownloadRunnableFallback implements Runnable {
|
||||||
mMission.setThreadBytePosition(0, start);
|
mMission.setThreadBytePosition(0, start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void interrupt() {
|
||||||
|
super.interrupt();
|
||||||
|
|
||||||
|
if (mConn != null) {
|
||||||
|
try {
|
||||||
|
mConn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,7 +235,7 @@ public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
if (icDownloadDone != null) icDownloadDone.recycle();
|
if (icDownloadDone != null) icDownloadDone.recycle();
|
||||||
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
||||||
icLauncher.recycle();
|
if (icLauncher != null) icLauncher.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in a new issue