implement resumable APK downloads for HttpDownloader

Moving towards having the Downloader always download directly into the
cache means its really easy to support resuming downloads.

This should also work for updates and icons, since it fetches the
Content Length via HTTP first.  The icon and update downloading code
needs to be adjusted to support that. For APKs, it probably makes
sense to include the file size from the index db to save the query
over the net.  That would be probably more helpful on Bluetooth.
This commit is contained in:
Hans-Christoph Steiner 2016-04-02 23:44:07 +02:00
parent fa766180bf
commit 3265c8634c
4 changed files with 55 additions and 23 deletions

View File

@ -80,7 +80,7 @@ public class BluetoothDownloader extends Downloader {
@Override
public void download() throws IOException, InterruptedException {
downloadFromStream(1024);
downloadFromStream(1024, false);
connection.closeQuietly();
}

View File

@ -31,8 +31,6 @@ public abstract class Downloader {
private volatile int totalBytes;
private Timer timer;
private final OutputStream outputStream;
public final File outputFile;
protected final URL sourceUrl;
@ -60,7 +58,6 @@ public abstract class Downloader {
throws FileNotFoundException, MalformedURLException {
this.sourceUrl = url;
outputFile = destFile;
outputStream = new FileOutputStream(outputFile);
}
public final InputStream getInputStream() throws IOException {
@ -100,9 +97,10 @@ public abstract class Downloader {
public abstract boolean isCached();
protected void downloadFromStream(int bufferSize) throws IOException, InterruptedException {
protected void downloadFromStream(int bufferSize, boolean resumable) throws IOException, InterruptedException {
Utils.debugLog(TAG, "Downloading from stream");
InputStream input = null;
OutputStream outputStream = new FileOutputStream(outputFile, resumable);
try {
input = getInputStream();
@ -110,7 +108,7 @@ public abstract class Downloader {
// we were interrupted before proceeding to the download.
throwExceptionIfInterrupted();
copyInputToOutputStream(input, bufferSize);
copyInputToOutputStream(input, bufferSize, outputStream);
} finally {
Utils.closeQuietly(outputStream);
Utils.closeQuietly(input);
@ -126,6 +124,7 @@ public abstract class Downloader {
* interrupt occured during that blocking operation. The goal is to ensure we
* don't move onto another slow, network operation if we have cancelled the
* download.
*
* @throws InterruptedException
*/
private void throwExceptionIfInterrupted() throws InterruptedException {
@ -150,7 +149,7 @@ public abstract class Downloader {
* keeping track of the number of bytes that have flowed through for the
* progress counter.
*/
private void copyInputToOutputStream(InputStream input, int bufferSize) throws IOException, InterruptedException {
private void copyInputToOutputStream(InputStream input, int bufferSize, OutputStream output) throws IOException, InterruptedException {
bytesRead = 0;
totalBytes = totalDownloadSize();
byte[] buffer = new byte[bufferSize];
@ -178,14 +177,13 @@ public abstract class Downloader {
Utils.debugLog(TAG, "Finished downloading from stream");
break;
}
bytesRead += count;
outputStream.write(buffer, 0, count);
output.write(buffer, 0, count);
}
timer.cancel();
timer.purge();
outputStream.flush();
outputStream.close();
output.flush();
output.close();
}
/**

View File

@ -2,6 +2,7 @@ package org.fdroid.fdroid.net;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.spongycastle.util.encoders.Base64;
@ -65,7 +66,7 @@ public class HttpDownloader extends Downloader {
*/
@Override
protected InputStream getDownloadersInputStream() throws IOException {
setupConnection();
setupConnection(false);
return new BufferedInputStream(connection.getInputStream());
}
@ -79,8 +80,25 @@ public class HttpDownloader extends Downloader {
*/
@Override
public void download() throws IOException, InterruptedException {
setupConnection();
doDownload();
boolean resumable = false;
long fileLength = outputFile.length();
// get the file size from the server
HttpURLConnection tmpConn = getConnection();
int contentLength = -1;
if (tmpConn.getResponseCode() == 200) {
contentLength = tmpConn.getContentLength();
}
tmpConn.disconnect();
if (fileLength > contentLength) {
FileUtils.deleteQuietly(outputFile);
} else if (fileLength == contentLength && outputFile.isFile()) {
return; // already have it!
} else if (fileLength > 0) {
resumable = true;
}
setupConnection(resumable);
doDownload(resumable);
}
private boolean isSwapUrl() {
@ -90,10 +108,8 @@ public class HttpDownloader extends Downloader {
&& FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are
}
protected void setupConnection() throws IOException {
if (connection != null) {
return;
}
private HttpURLConnection getConnection() throws IOException {
HttpURLConnection connection;
if (isSwapUrl()) {
// swap never works with a proxy, its unrouted IP on the same subnet
connection = (HttpURLConnection) sourceUrl.openConnection();
@ -113,9 +129,25 @@ public class HttpDownloader extends Downloader {
String authString = username + ":" + password;
connection.setRequestProperty("Authorization", "Basic " + Base64.toBase64String(authString.getBytes()));
}
return connection;
}
protected void doDownload() throws IOException, InterruptedException {
/**
* @return Whether the connection is resumable or not
*/
protected void setupConnection(boolean resumable) throws IOException {
if (connection != null) {
return;
}
connection = getConnection();
if (resumable) {
// partial file exists, resume the download
connection.setRequestProperty("Range", "bytes=" + outputFile.length() + "-");
}
}
protected void doDownload(boolean resumable) throws IOException, InterruptedException {
if (wantToCheckCache()) {
setupCacheCheck();
Utils.debugLog(TAG, "Checking cached status of " + sourceUrl);
@ -125,8 +157,8 @@ public class HttpDownloader extends Downloader {
if (isCached()) {
Utils.debugLog(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
} else {
Utils.debugLog(TAG, "Downloading from " + sourceUrl);
downloadFromStream(4096);
Utils.debugLog(TAG, "doDownload for " + sourceUrl + " " + resumable);
downloadFromStream(8192, resumable);
updateCacheCheck();
}
}
@ -166,6 +198,8 @@ public class HttpDownloader extends Downloader {
@Override
public void close() {
connection.disconnect();
if (connection != null) {
connection.disconnect();
}
}
}

View File

@ -43,7 +43,7 @@ public class LocalFileDownloader extends Downloader {
@Override
public void download() throws IOException, InterruptedException {
downloadFromStream(1024 * 50);
downloadFromStream(1024 * 50, false);
}
@Override