Created
August 15, 2017 09:08
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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