Skip to content

Instantly share code, notes, and snippets.

@AlexMeuer
Created August 15, 2017 09:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlexMeuer/336145770dbcc4d3c26565f95fefaaa8 to your computer and use it in GitHub Desktop.
Save AlexMeuer/336145770dbcc4d3c26565f95fefaaa8 to your computer and use it in GitHub Desktop.
Downloads files one by one to the app's internal storage. Logs a warning if a file already exists but does not stop the download. Uses guava for string testing.
package foo.bar
import android.content.Context;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Log;
import com.google.common.base.Strings;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* Downloads files one after another. Only one is downloaded at a time.
*/
public class SequentialFileDownloader {
public static final String TAG = "SequentialDownloader";
private final Context mContext;
private final ExecutorService mDownloadExecutor;
private final ExecutorService mNotificationExecutor;
public SequentialFileDownloader(@NonNull Context context) {
mContext = context;
mDownloadExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable target) {
return new Thread(target, TAG+"_worker");
}
});
mNotificationExecutor = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable target) {
return new Thread(target, TAG+"_notifier");
}
});
}
@SuppressWarnings("squid:S1226") // S1226 warns about reassignment to a parameter; we are defaulting 'dstFileName' to 'srcFileName' is it is null or empty.
public SequentialFileDownloader add(String baseUrl, String srcFileName, @Nullable String dstFileName, @Nullable DownloadListener listener) {
// mBaseUrl and fileName must both be non-empty strings.
if(Strings.isNullOrEmpty(baseUrl) || Strings.isNullOrEmpty(srcFileName)) {
Log.w(TAG, "Cannot add download: mBaseUrl or fileName is missing! ["+baseUrl+"/"+srcFileName+"]");
return this;
}
if (Strings.isNullOrEmpty(dstFileName)) {
dstFileName = srcFileName;
}
// Log a warning if the file already exists.
final File file = mContext.getFileStreamPath(dstFileName);
if (file.exists()) {
Log.w(TAG, "When download begins, old file will be overwritten! ("+dstFileName+")");
}
// Queue the file for download.
mDownloadExecutor.submit(new DownloadRunnable(baseUrl, srcFileName, dstFileName, listener));
return this;
}
public interface DownloadListener {
/** Invoked when downloading commences. */
@WorkerThread void onBegin(int fileSize);
/** Invoked whenever progress changes. This may be invoked in very quick succession and is not rate-limited. */
@WorkerThread void onProgress(@FloatRange(from=0.0,to=1.0) float progress);
/** Invoked when download finishes successfully. */
@WorkerThread void onComplete();
/** Invoked when the file cannot be downloaded. */
@WorkerThread void onFailure(@NonNull IOException e);
}
private class DownloadRunnable implements Runnable {
private final String mBaseUrl;
private final String mSrcFileName;
private final String mDstFileName;
private final DownloadListener mListener;
/**
* @param baseUrl The base url to get the file from.
* @param srcFileName The name of the file to append to the mBaseUrl.
* @param dstFileName The name to save the downloaded file as.
* @param listener The event listener for this download. Can be null.
*/
DownloadRunnable(@NonNull String baseUrl,
@NonNull String srcFileName,
@NonNull String dstFileName,
@Nullable DownloadListener listener) {
final String separator = "/";
if (baseUrl.endsWith(separator)) {
mBaseUrl = baseUrl;
} else {
mBaseUrl = baseUrl + separator;
}
mSrcFileName = srcFileName;
mDstFileName = dstFileName;
mListener = listener;
}
@Override
public void run() {
try {
downloadFile();
} catch (IOException e) {
notifyFailure(e);
}
}
@WorkerThread
private void downloadFile() throws IOException {
final URL url = new URL(mBaseUrl + mSrcFileName);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
final BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream());
// Ensure that the outputStream will be closed even if an exception is thrown.
try (final BufferedOutputStream outputStream = new BufferedOutputStream(
mContext.openFileOutput(mDstFileName, Context.MODE_PRIVATE)))
{
final int fileSize = connection.getContentLength();
notifyBegin(fileSize);
// Read from the input stream and write that data to the output stream.
byte[] buffer = new byte[1024];
int totalBytesRead = 0;
int bytesRead = inputStream.read(buffer);
while (0 < bytesRead) {
totalBytesRead += bytesRead;
outputStream.write(buffer, 0, bytesRead);
notifyProgress(totalBytesRead, fileSize);
bytesRead = inputStream.read(buffer);
}
outputStream.flush(); // flush the buffered output stream to file
notifyComplete();
}
}
/**
* Notifies the listener (if we have one) that we have begun to download the file.
* @param fileSize The size of the file to be downloaded.
*/
private void notifyBegin(final int fileSize) {
Log.v(TAG, "Beginning download of '"+mSrcFileName +"'->'"+mDstFileName+"'. Size: "+fileSize);
if (null != mListener) {
mNotificationExecutor.submit(new Runnable() {
@Override public void run() {
mListener.onBegin(fileSize);
}
});
}
}
/**
* Notifies the listener (if we have one) of download progress.
* @param bytesComplete The number of bytes we have downloaded.
* @param totalBytes The size of the file in bytes.
*/
private void notifyProgress(final int bytesComplete, final int totalBytes) {
if (null != mListener) {
mNotificationExecutor.submit(new Runnable() {
@Override public void run() {
mListener.onProgress(bytesComplete / (float) totalBytes);
}
});
}
}
private void notifyComplete() {
Log.v(TAG, "Download complete: '"+mSrcFileName +"'->'"+mDstFileName+"'");
if (null != mListener) {
mNotificationExecutor.submit(new Runnable() {
@Override public void run() {
mListener.onComplete();
}
});
}
}
private void notifyFailure(@NonNull final IOException e) {
Log.e(TAG, "Failed to download '"+mSrcFileName +"'->'"+mDstFileName+"'", e);
if (null != mListener) {
mNotificationExecutor.submit(new Runnable() {
@Override public void run() {
mListener.onFailure(e);
}
});
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment