package org.fdroid.fdroid.net;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.Utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Timer;
import java.util.TimerTask;
public abstract class Downloader {
private static final String TAG = "Downloader";
public static final String ACTION_STARTED = "org.fdroid.fdroid.net.Downloader.action.STARTED";
public static final String ACTION_PROGRESS = "org.fdroid.fdroid.net.Downloader.action.PROGRESS";
public static final String ACTION_INTERRUPTED = "org.fdroid.fdroid.net.Downloader.action.INTERRUPTED";
public static final String ACTION_COMPLETE = "org.fdroid.fdroid.net.Downloader.action.COMPLETE";
public static final String EXTRA_DOWNLOAD_PATH = "org.fdroid.fdroid.net.Downloader.extra.DOWNLOAD_PATH";
public static final String EXTRA_BYTES_READ = "org.fdroid.fdroid.net.Downloader.extra.BYTES_READ";
public static final String EXTRA_TOTAL_BYTES = "org.fdroid.fdroid.net.Downloader.extra.TOTAL_BYTES";
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE";
private volatile boolean cancelled = false;
private volatile int bytesRead;
private volatile int totalBytes;
public final File outputFile;
final URL sourceUrl;
String cacheTag;
/**
* For sending download progress, should only be called in {@link #progressTask}
*/
private volatile ProgressListener downloaderProgressListener;
protected abstract InputStream getDownloadersInputStream() throws IOException;
protected abstract void close();
Downloader(URL url, File destFile) {
this.sourceUrl = url;
outputFile = destFile;
}
public final InputStream getInputStream() throws IOException {
return new WrappedInputStream(getDownloadersInputStream());
}
public void setListener(ProgressListener listener) {
this.downloaderProgressListener = listener;
}
/**
* If you ask for the cacheTag before calling download(), you will get the
* same one you passed in (if any). If you call it after download(), you
* will get the new cacheTag from the server, or null if there was none.
*/
public String getCacheTag() {
return cacheTag;
}
/**
* If this cacheTag matches that returned by the server, then no download will
* take place, and a status code of 304 will be returned by download().
*/
public void setCacheTag(String cacheTag) {
this.cacheTag = cacheTag;
}
boolean wantToCheckCache() {
return cacheTag != null;
}
public abstract boolean hasChanged();
protected abstract int totalDownloadSize();
public abstract void download() throws IOException, InterruptedException;
public abstract boolean isCached();
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();
// Getting the input stream is slow(ish) for HTTP downloads, so we'll check if
// we were interrupted before proceeding to the download.
throwExceptionIfInterrupted();
copyInputToOutputStream(input, bufferSize, outputStream);
} finally {
Utils.closeQuietly(outputStream);
Utils.closeQuietly(input);
}
// Even if we have completely downloaded the file, we should probably respect
// the wishes of the user who wanted to cancel us.
throwExceptionIfInterrupted();
}
/**
* After every network operation that could take a while, we will check if an
* 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 {
if (cancelled) {
Utils.debugLog(TAG, "Received interrupt, cancelling download");
throw new InterruptedException();
}
}
/**
* Cancel a running download, triggering an {@link InterruptedException}
*/
public void cancelDownload() {
cancelled = true;
}
/**
* This copies the downloaded data from the InputStream to the OutputStream,
* keeping track of the number of bytes that have flowed through for the
* progress counter.
*/
private void copyInputToOutputStream(InputStream input, int bufferSize, OutputStream output) throws IOException, InterruptedException {
Timer timer = new Timer();
try {
bytesRead = 0;
totalBytes = totalDownloadSize();
byte[] buffer = new byte[bufferSize];
timer.scheduleAtFixedRate(progressTask, 0, 100);
// Getting the total download size could potentially take time, depending on how
// it is implemented, so we may as well check this before we proceed.
throwExceptionIfInterrupted();
while (true) {
int count;
if (input.available() > 0) {
int readLength = Math.min(input.available(), buffer.length);
count = input.read(buffer, 0, readLength);
} else {
count = input.read(buffer);
}
throwExceptionIfInterrupted();
if (count == -1) {
Utils.debugLog(TAG, "Finished downloading from stream");
break;
}
bytesRead += count;
output.write(buffer, 0, count);
}
} finally {
downloaderProgressListener = null;
timer.cancel();
timer.purge();
output.flush();
output.close();
}
}
/**
* Send progress updates on a timer to avoid flooding receivers with pointless events.
*/
private final TimerTask progressTask = new TimerTask() {
@Override
public void run() {
if (downloaderProgressListener != null) {
downloaderProgressListener.onProgress(sourceUrl, bytesRead, totalBytes);
}
}
};
/**
* Overrides every method in {@link InputStream} and delegates to the wrapped stream.
* The only difference is that when we call the {@link WrappedInputStream#close()} method,
* after delegating to the wrapped stream we invoke the {@link Downloader#close()} method
* on the {@link Downloader} which created this.
*/
private class WrappedInputStream extends InputStream {
private final InputStream toWrap;
WrappedInputStream(InputStream toWrap) {
super();
this.toWrap = toWrap;
}
@Override
public void close() throws IOException {
toWrap.close();
Downloader.this.close();
}
@Override
public int available() throws IOException {
return toWrap.available();
}
@Override
public void mark(int readlimit) {
toWrap.mark(readlimit);
}
@Override
public boolean markSupported() {
return toWrap.markSupported();
}
@Override
public int read(byte[] buffer) throws IOException {
return toWrap.read(buffer);
}
@Override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
return toWrap.read(buffer, byteOffset, byteCount);
}
@Override
public synchronized void reset() throws IOException {
toWrap.reset();
}
@Override
public long skip(long byteCount) throws IOException {
return toWrap.skip(byteCount);
}
@Override
public int read() throws IOException {
return toWrap.read();
}
}
}