package com.thin.downloadmanager; import android.os.Process; import com.thin.downloadmanager.util.Log; import org.apache.http.conn.ConnectTimeoutException; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.BlockingQueue; import static android.content.ContentValues.TAG; import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_MOVED_PERM; import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_PARTIAL; import static java.net.HttpURLConnection.HTTP_SEE_OTHER; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; /** * This thread/class used to make {@link HttpURLConnection}, receives Response from server * and Dispatch the response to respective {@link DownloadRequest} * * @author Mani Selvaraj * @author Praveen Kumar */ class DownloadDispatcher extends Thread { /** * The queue of download requests to service. */ private final BlockingQueue<DownloadRequest> mQueue; /** * The buffer size used to stream the data */ private final int BUFFER_SIZE = 4096; /** * The maximum number of redirects. */ private final int MAX_REDIRECTS = 5; // can't be more than 7. private final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; private final int HTTP_TEMP_REDIRECT = 307; /** * Used to tell the dispatcher to die. */ private volatile boolean mQuit = false; /** * To Delivery call back response on main thread */ private DownloadRequestQueue.CallBackDelivery mDelivery; /** * How many times redirects happened during a download request. */ private int mRedirectionCount = 0; private long mContentLength; private boolean shouldAllowRedirects = true; /** * This variable is part of resumable download feature. * It will load the downloaded file cache length, If It had been already in available Downloaded Requested output path. * Otherwise it would keep "0" always by default. */ private long mDownloadedCacheSize = 0; private Timer mTimer; /** * Constructor take the dependency (DownloadRequest queue) that all the Dispatcher needs */ DownloadDispatcher(BlockingQueue<DownloadRequest> queue, DownloadRequestQueue.CallBackDelivery delivery) { mQueue = queue; mDelivery = delivery; } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); mTimer = new Timer(); while (true) { DownloadRequest request = null; try { request = mQueue.take(); mRedirectionCount = 0; shouldAllowRedirects = true; Log.v("Download initiated for " + request.getDownloadId()); updateDownloadState(request, DownloadManager.STATUS_STARTED); executeDownload(request, request.getUri().toString()); } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { if (request != null) { request.finish(); // don't remove files that have been downloaded sucessfully. if (request.getDownloadState() != DownloadManager.STATUS_SUCCESSFUL) { updateDownloadFailed(request, DownloadManager.ERROR_DOWNLOAD_CANCELLED, "Download cancelled"); } } mTimer.cancel(); return; } } } } void quit() { mQuit = true; interrupt(); } private void executeDownload(DownloadRequest request, String downloadUrl) { URL url; try { url = new URL(downloadUrl); } catch (MalformedURLException e) { updateDownloadFailed(request, DownloadManager.ERROR_MALFORMED_URI, "MalformedURLException: URI passed is malformed."); return; } HttpURLConnection conn = null; try { conn = (HttpURLConnection) url.openConnection(); File destinationFile = new File(request.getDestinationURI().getPath()); if (destinationFile.exists()) { mDownloadedCacheSize = (int) destinationFile.length(); } conn.setRequestProperty("Range", "bytes=" + mDownloadedCacheSize + "-"); Log.d(TAG, "Existing file mDownloadedCacheSize: " + mDownloadedCacheSize); conn.setInstanceFollowRedirects(false); conn.setConnectTimeout(request.getRetryPolicy().getCurrentTimeout()); conn.setReadTimeout(request.getRetryPolicy().getCurrentTimeout()); HashMap<String, String> customHeaders = request.getCustomHeaders(); if (customHeaders != null) { for (String headerName : customHeaders.keySet()) { conn.addRequestProperty(headerName, customHeaders.get(headerName)); } } // Status Connecting is set here before // urlConnection is trying to connect to destination. updateDownloadState(request, DownloadManager.STATUS_CONNECTING); final int responseCode = conn.getResponseCode(); Log.v("Response code obtained for downloaded Id " + request.getDownloadId() + " : httpResponse Code " + responseCode); switch (responseCode) { case HTTP_PARTIAL: case HTTP_OK: shouldAllowRedirects = false; if (readResponseHeaders(request, conn, responseCode) == 1) { Log.d(TAG, "Existing mDownloadedCacheSize: " + mDownloadedCacheSize); Log.d(TAG, "File mContentLength: " + mContentLength); if (mDownloadedCacheSize == mContentLength) { // Mark as success, If end of stream already reached updateDownloadComplete(request); Log.d(TAG, "Download Completed"); } else { transferData(request, conn); } } else { updateDownloadFailed(request, DownloadManager.ERROR_DOWNLOAD_SIZE_UNKNOWN, "Transfer-Encoding not found as well as can't know size of download, giving up"); } return; case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER: case HTTP_TEMP_REDIRECT: // Take redirect url and call executeDownload recursively until // MAX_REDIRECT is reached. while (mRedirectionCount < MAX_REDIRECTS && shouldAllowRedirects) { mRedirectionCount++; Log.v(TAG, "Redirect for downloaded Id " + request.getDownloadId()); final String location = conn.getHeaderField("Location"); executeDownload(request, location); } if (mRedirectionCount > MAX_REDIRECTS && shouldAllowRedirects) { updateDownloadFailed(request, DownloadManager.ERROR_TOO_MANY_REDIRECTS, "Too many redirects, giving up"); return; } break; case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: updateDownloadFailed(request, HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, conn.getResponseMessage()); break; case HTTP_UNAVAILABLE: updateDownloadFailed(request, HTTP_UNAVAILABLE, conn.getResponseMessage()); break; case HTTP_INTERNAL_ERROR: updateDownloadFailed(request, HTTP_INTERNAL_ERROR, conn.getResponseMessage()); break; default: updateDownloadFailed(request, DownloadManager.ERROR_UNHANDLED_HTTP_CODE, "Unhandled HTTP response:" + responseCode + " message:" + conn.getResponseMessage()); break; } } catch (SocketTimeoutException e) { e.printStackTrace(); // Retry. attemptRetryOnTimeOutException(request); } catch (ConnectTimeoutException e) { e.printStackTrace(); attemptRetryOnTimeOutException(request); } catch (IOException e) { e.printStackTrace(); updateDownloadFailed(request, DownloadManager.ERROR_HTTP_DATA_ERROR, "Trouble with low-level sockets"); } finally { if (conn != null) { conn.disconnect(); } } } private void transferData(DownloadRequest request, HttpURLConnection conn) { BufferedInputStream in = null; RandomAccessFile accessFile = null; cleanupDestination(request, false); try { try { in = new BufferedInputStream(conn.getInputStream()); } catch (IOException e) { e.printStackTrace(); } File destinationFile = new File(request.getDestinationURI().getPath()); boolean errorCreatingDestinationFile = false; // Create destination file if it doesn't exists if (!destinationFile.exists()) { try { // Check path File parentPath = destinationFile.getParentFile(); if (parentPath != null && !parentPath.exists()) { parentPath.mkdirs(); } if (!destinationFile.createNewFile()) { errorCreatingDestinationFile = true; updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in creating destination file"); } } catch (IOException e) { e.printStackTrace(); errorCreatingDestinationFile = true; updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in creating destination file"); } } else { if (in != null) { request.abortCancel(); } } // If Destination file couldn't be created. Abort the data transfer. if (!errorCreatingDestinationFile) { try { accessFile = new RandomAccessFile(destinationFile, "rw"); accessFile.seek(mDownloadedCacheSize); } catch (IOException e) { e.printStackTrace(); } if (in == null) { updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in creating input stream"); } else if (accessFile == null) { updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in writing download contents to the destination file"); } else { // Start streaming data transferData(request, in, accessFile); } } } finally { try { if (in != null) { in.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (accessFile != null) { accessFile.close(); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (accessFile != null) accessFile.close(); } catch (IOException e) { e.printStackTrace(); } } } } private void transferData(DownloadRequest request, InputStream in, RandomAccessFile out) { final byte data[] = new byte[BUFFER_SIZE]; long mCurrentBytes = mDownloadedCacheSize; request.setDownloadState(DownloadManager.STATUS_RUNNING); Log.v("Content Length: " + mContentLength + " for Download Id " + request.getDownloadId()); for (; ; ) { if (request.isCancelled()) { Log.v("Stopping the download as Download Request is cancelled for Downloaded Id " + request.getDownloadId()); request.finish(); updateDownloadFailed(request, DownloadManager.ERROR_DOWNLOAD_CANCELLED, "Download cancelled"); return; } int bytesRead = readFromResponse(request, data, in); if (mContentLength != -1 && mContentLength > 0) { int progress = (int) ((mCurrentBytes * 100) / mContentLength); updateDownloadProgress(request, progress, mCurrentBytes); } if (bytesRead == -1) { // success, end of stream already reached updateDownloadComplete(request); return; } else if (bytesRead == Integer.MIN_VALUE) { return; } if (writeDataToDestination(request, data, bytesRead, out)) { mCurrentBytes += bytesRead; } else { request.finish(); updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Failed writing file"); return; } } } private int readFromResponse(DownloadRequest request, byte[] data, InputStream entityStream) { try { return entityStream.read(data); } catch (IOException ex) { if ("unexpected end of stream".equals(ex.getMessage())) { return -1; } updateDownloadFailed(request, DownloadManager.ERROR_HTTP_DATA_ERROR, "IOException: Failed reading response"); return Integer.MIN_VALUE; } } private boolean writeDataToDestination(DownloadRequest request, byte[] data, int bytesRead, RandomAccessFile out) { boolean successInWritingToDestination = true; try { out.write(data, 0, bytesRead); } catch (IOException ex) { updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "IOException when writing download contents to the destination file"); successInWritingToDestination = false; } catch (Exception e) { updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Exception when writing download contents to the destination file"); successInWritingToDestination = false; } return successInWritingToDestination; } private int readResponseHeaders(DownloadRequest request, HttpURLConnection conn, int responseCode) { final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); mContentLength = -1; if (transferEncoding == null) { if (responseCode == HTTP_OK) { // If file download already completed, 200 HttpStatusCode will thrown by service. mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); } else { // If file download already partially completed, 206 HttpStatusCode will thrown by service and we can resume remaining chunks downloads. mContentLength = getHeaderFieldLong(conn, "Content-Length", -1) + mDownloadedCacheSize; } } else { Log.v("Ignoring Content-Length since Transfer-Encoding is also defined for Downloaded Id " + request.getDownloadId()); } if (mContentLength != -1) { return 1; } else if (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")) { return -1; } else { return 1; } } private long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { try { return Long.parseLong(conn.getHeaderField(field)); } catch (NumberFormatException e) { return defaultValue; } } private void attemptRetryOnTimeOutException(final DownloadRequest request) { updateDownloadState(request, DownloadManager.STATUS_RETRYING); final RetryPolicy retryPolicy = request.getRetryPolicy(); try { retryPolicy.retry(); mTimer.schedule(new TimerTask() { @Override public void run() { executeDownload(request, request.getUri().toString()); } }, retryPolicy.getCurrentTimeout()); } catch (RetryError e) { // Update download failed. updateDownloadFailed(request, DownloadManager.ERROR_CONNECTION_TIMEOUT_AFTER_RETRIES, "Connection time out after maximum retires attempted"); } } /** * Called just before the thread finishes, regardless of status, to take any necessary action on * the downloaded file with mDownloadedCacheSize file. * * @param forceClean - It will delete downloaded cache, Even streaming is enabled, If user intentionally cancelled. */ private void cleanupDestination(DownloadRequest request, boolean forceClean) { if (!request.isResumable() || forceClean) { Log.d("cleanupDestination() deleting " + request.getDestinationURI().getPath()); File destinationFile = new File(request.getDestinationURI().getPath()); if (destinationFile.exists()) { destinationFile.delete(); } } } private void updateDownloadState(DownloadRequest request, int state) { request.setDownloadState(state); } private void updateDownloadComplete(DownloadRequest request) { mDownloadedCacheSize = 0; // reset into Zero. mDelivery.postDownloadComplete(request); request.setDownloadState(DownloadManager.STATUS_SUCCESSFUL); request.finish(); } private void updateDownloadFailed(DownloadRequest request, int errorCode, String errorMsg) { mDownloadedCacheSize = 0; // reset into Zero. shouldAllowRedirects = false; request.setDownloadState(DownloadManager.STATUS_FAILED); if (request.getDeleteDestinationFileOnFailure()) { cleanupDestination(request, true); } mDelivery.postDownloadFailed(request, errorCode, errorMsg); request.finish(); } private void updateDownloadProgress(DownloadRequest request, int progress, long downloadedBytes) { mDelivery.postProgressUpdate(request, mContentLength, downloadedBytes, progress); } }