diff options
author | Ricardo Cerqueira <cyanogenmod@cerqueira.org> | 2013-07-25 00:46:33 +0100 |
---|---|---|
committer | Ricardo Cerqueira <cyanogenmod@cerqueira.org> | 2013-07-25 00:46:33 +0100 |
commit | 8834e86030ab48890308b3aaa005c11cf968424f (patch) | |
tree | 33e7361d00da57cf7e4e7305cb5d849773671a74 /src/com/android/providers/downloads/DownloadThread.java | |
parent | 459fc32974f03d27a7b70ea8acadbc44e2360f11 (diff) | |
parent | 2435eb9c89bd46511624b9d89ab22766f82dbfcb (diff) | |
download | android_packages_providers_DownloadProvider-8834e86030ab48890308b3aaa005c11cf968424f.tar.gz android_packages_providers_DownloadProvider-8834e86030ab48890308b3aaa005c11cf968424f.tar.bz2 android_packages_providers_DownloadProvider-8834e86030ab48890308b3aaa005c11cf968424f.zip |
Merge tag 'android-4.3_r2.1' into cm-10.2
Android 4.3 release 2.1
Conflicts:
res/values-cs/strings.xml
Change-Id: Idceee08cb4d6f69e4f8cc4c043142852ac932e7f
Diffstat (limited to 'src/com/android/providers/downloads/DownloadThread.java')
-rw-r--r-- | src/com/android/providers/downloads/DownloadThread.java | 889 |
1 files changed, 387 insertions, 502 deletions
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java index 34bc8e34..6a0eb47e 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -16,16 +16,33 @@ package com.android.providers.downloads; +import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST; +import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME; +import static android.provider.Downloads.Impl.STATUS_FILE_ERROR; +import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR; +import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; +import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; +import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.providers.downloads.Constants.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; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.drm.DrmManagerClient; +import android.drm.DrmOutputStream; +import android.net.ConnectivityManager; import android.net.INetworkPolicyListener; +import android.net.NetworkInfo; import android.net.NetworkPolicyManager; -import android.net.Proxy; import android.net.TrafficStats; -import android.net.http.AndroidHttpClient; import android.os.FileUtils; import android.os.PowerManager; import android.os.Process; @@ -35,39 +52,51 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.params.ConnRouteParams; +import com.android.providers.downloads.DownloadInfo.NetworkState; import java.io.File; -import java.io.FileNotFoundException; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.SyncFailedException; -import java.net.URI; -import java.net.URISyntaxException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +import libcore.io.IoUtils; /** - * Runs an actual download + * Task which executes a given {@link DownloadInfo}: making network requests, + * persisting data to disk, and updating {@link DownloadProvider}. */ -public class DownloadThread extends Thread { +public class DownloadThread implements Runnable { + + // TODO: bind each download to a specific network interface to avoid state + // checking races once we have ConnectivityManager API + + private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + private static final int HTTP_TEMP_REDIRECT = 307; + + private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); private final Context mContext; private final DownloadInfo mInfo; private final SystemFacade mSystemFacade; private final StorageManager mStorageManager; - private DrmConvertSession mDrmConvertSession; + private final DownloadNotifier mNotifier; private volatile boolean mPolicyDirty; public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, - StorageManager storageManager) { + StorageManager storageManager, DownloadNotifier notifier) { mContext = context; mSystemFacade = systemFacade; mInfo = info; mStorageManager = storageManager; + mNotifier = notifier; } /** @@ -86,12 +115,8 @@ public class DownloadThread extends Thread { */ static class State { public String mFilename; - public FileOutputStream mStream; public String mMimeType; - public boolean mCountRetry = false; public int mRetryAfter = 0; - public int mRedirectCount = 0; - public String mNewUri; public boolean mGotData = false; public String mRequestUri; public long mTotalBytes = -1; @@ -100,6 +125,7 @@ public class DownloadThread extends Thread { public boolean mContinuingDownload = false; public long mBytesNotified = 0; public long mTimeLastNotification = 0; + public int mNetworkType = ConnectivityManager.TYPE_NONE; /** Historical bytes/second speed of this download. */ public long mSpeed; @@ -108,6 +134,13 @@ public class DownloadThread extends Thread { /** Bytes transferred since current sample started. */ public long mSpeedSampleBytes; + public long mContentLength = -1; + public String mContentDisposition; + public String mContentLocation; + + public int mRedirectionCount; + public URL mUrl; + public State(DownloadInfo info) { mMimeType = Intent.normalizeMimeType(info.mMimeType); mRequestUri = info.mUri; @@ -115,33 +148,23 @@ public class DownloadThread extends Thread { mTotalBytes = info.mTotalBytes; mCurrentBytes = info.mCurrentBytes; } - } - /** - * State within executeDownload() - */ - private static class InnerState { - public String mHeaderContentLength; - public String mHeaderContentDisposition; - public String mHeaderContentLocation; + public void resetBeforeExecute() { + // Reset any state from previous execution + mContentLength = -1; + mContentDisposition = null; + mContentLocation = null; + mRedirectionCount = 0; + } } - /** - * Raised from methods called by executeDownload() to indicate that the download should be - * retried immediately. - */ - private class RetryDownload extends Throwable {} - - /** - * Executes the download in a separate thread - */ @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); try { runInternal(); } finally { - DownloadHandler.getInstance().dequeueDownload(mInfo.mId); + mNotifier.notifyDownloadSpeed(mInfo.mId, 0); } } @@ -155,9 +178,9 @@ public class DownloadThread extends Thread { } State state = new State(mInfo); - AndroidHttpClient client = null; PowerManager.WakeLock wakeLock = null; int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; + int numFailed = mInfo.mNumFailed; String errorMsg = null; final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext); @@ -170,39 +193,29 @@ public class DownloadThread extends Thread { // while performing download, register for rules updates netPolicy.registerListener(mPolicyListener); - if (Constants.LOGV) { - Log.v(Constants.TAG, "initiating download for " + mInfo.mUri); - } + Log.i(Constants.TAG, "Download " + mInfo.mId + " starting"); - client = AndroidHttpClient.newInstance(userAgent(), mContext); + // Remember which network this download started on; used to + // determine if errors were due to network changes. + final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); + if (info != null) { + state.mNetworkType = info.getType(); + } - // network traffic on this thread should be counted against the - // requesting uid, and is tagged with well-known value. + // Network traffic on this thread should be counted against the + // requesting UID, and is tagged with well-known value. TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); TrafficStats.setThreadStatsUid(mInfo.mUid); - boolean finished = false; - while(!finished) { - Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId); - // Set or unset proxy, which may have changed since last GET request. - // setDefaultProxy() supports null as proxy parameter. - ConnRouteParams.setDefaultProxy(client.getParams(), - Proxy.getPreferredHttpHost(mContext, state.mRequestUri)); - HttpGet request = new HttpGet(state.mRequestUri); - try { - executeDownload(state, client, request); - finished = true; - } catch (RetryDownload exc) { - // fall through - } finally { - request.abort(); - request = null; - } + try { + // TODO: migrate URL sanity checking into client side of API + state.mUrl = new URL(state.mRequestUri); + } catch (MalformedURLException e) { + throw new StopRequestException(STATUS_BAD_REQUEST, e); } - if (Constants.LOGV) { - Log.v(Constants.TAG, "download completed for " + mInfo.mUri); - } + executeDownload(state); + finalizeDestinationFile(state); finalStatus = Downloads.Impl.STATUS_SUCCESS; } catch (StopRequestException error) { @@ -213,9 +226,37 @@ public class DownloadThread extends Thread { if (Constants.LOGV) { Log.w(Constants.TAG, msg, error); } - finalStatus = error.mFinalStatus; + finalStatus = error.getFinalStatus(); + + // Nobody below our level should request retries, since we handle + // failure counts at this level. + if (finalStatus == STATUS_WAITING_TO_RETRY) { + throw new IllegalStateException("Execution should always throw final error codes"); + } + + // Some errors should be retryable, unless we fail too many times. + if (isStatusRetryable(finalStatus)) { + if (state.mGotData) { + numFailed = 1; + } else { + numFailed += 1; + } + + if (numFailed < Constants.MAX_RETRIES) { + final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); + if (info != null && info.getType() == state.mNetworkType + && info.isConnected()) { + // Underlying network is still intact, use normal backoff + finalStatus = STATUS_WAITING_TO_RETRY; + } else { + // Network changed, retry on any next available + finalStatus = STATUS_WAITING_FOR_NETWORK; + } + } + } + // fall through to finally block - } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions + } catch (Throwable ex) { errorMsg = ex.getMessage(); String msg = "Exception for id " + mInfo.mId + ": " + errorMsg; Log.w(Constants.TAG, msg, ex); @@ -225,14 +266,11 @@ public class DownloadThread extends Thread { TrafficStats.clearThreadStatsTag(); TrafficStats.clearThreadStatsUid(); - if (client != null) { - client.close(); - client = null; - } cleanupDestination(state, finalStatus); - notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, - state.mGotData, state.mFilename, - state.mNewUri, state.mMimeType, errorMsg); + notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed); + + Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status " + + Downloads.Impl.statusToString(finalStatus)); netPolicy.unregisterListener(mPolicyListener); @@ -245,16 +283,12 @@ public class DownloadThread extends Thread { } /** - * Fully execute a single download request - setup and send the request, handle the response, - * and transfer the data to the destination file. + * Fully execute a single download request. Setup and send the request, + * handle the response, and transfer the data to the destination file. */ - private void executeDownload(State state, AndroidHttpClient client, HttpGet request) - throws StopRequestException, RetryDownload { - InnerState innerState = new InnerState(); - byte data[] = new byte[Constants.BUFFER_SIZE]; - - setupDestinationFile(state, innerState); - addRequestHeaders(state, request); + private void executeDownload(State state) throws StopRequestException { + state.resetBeforeExecute(); + setupDestinationFile(state); // skip when already finished; remove after fixing race in 5217390 if (state.mCurrentBytes == state.mTotalBytes) { @@ -263,19 +297,136 @@ public class DownloadThread extends Thread { return; } - // check just before sending the request to avoid using an invalid connection at all - checkConnectivity(); - - HttpResponse response = sendRequest(state, client, request); - handleExceptionalStatus(state, innerState, response); + while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) { + // Open connection and follow any redirects until we have a useful + // response with body. + HttpURLConnection conn = null; + try { + checkConnectivity(); + conn = (HttpURLConnection) state.mUrl.openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setConnectTimeout(DEFAULT_TIMEOUT); + conn.setReadTimeout(DEFAULT_TIMEOUT); + + addRequestHeaders(state, conn); + + final int responseCode = conn.getResponseCode(); + switch (responseCode) { + case HTTP_OK: + if (state.mContinuingDownload) { + throw new StopRequestException( + STATUS_CANNOT_RESUME, "Expected partial, but received OK"); + } + processResponseHeaders(state, conn); + transferData(state, conn); + return; + + case HTTP_PARTIAL: + if (!state.mContinuingDownload) { + throw new StopRequestException( + STATUS_CANNOT_RESUME, "Expected OK, but received partial"); + } + transferData(state, conn); + return; + + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_SEE_OTHER: + case HTTP_TEMP_REDIRECT: + final String location = conn.getHeaderField("Location"); + state.mUrl = new URL(state.mUrl, location); + if (responseCode == HTTP_MOVED_PERM) { + // Push updated URL back to database + state.mRequestUri = state.mUrl.toString(); + } + continue; + + case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: + throw new StopRequestException( + STATUS_CANNOT_RESUME, "Requested range not satisfiable"); + + case HTTP_UNAVAILABLE: + parseRetryAfterHeaders(state, conn); + throw new StopRequestException( + HTTP_UNAVAILABLE, conn.getResponseMessage()); + + case HTTP_INTERNAL_ERROR: + throw new StopRequestException( + HTTP_INTERNAL_ERROR, conn.getResponseMessage()); + + default: + StopRequestException.throwUnhandledHttpError( + responseCode, conn.getResponseMessage()); + } + } catch (IOException e) { + // Trouble with low-level sockets + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); - if (Constants.LOGV) { - Log.v(Constants.TAG, "received response for " + mInfo.mUri); + } finally { + if (conn != null) conn.disconnect(); + } } - processResponseHeaders(state, innerState, response); - InputStream entityStream = openResponseEntity(state, response); - transferData(state, innerState, data, entityStream); + throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects"); + } + + /** + * Transfer data from the given connection to the destination file. + */ + private void transferData(State state, HttpURLConnection conn) throws StopRequestException { + DrmManagerClient drmClient = null; + InputStream in = null; + OutputStream out = null; + FileDescriptor outFd = null; + try { + try { + in = conn.getInputStream(); + } catch (IOException e) { + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); + } + + try { + if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) { + drmClient = new DrmManagerClient(mContext); + final RandomAccessFile file = new RandomAccessFile( + new File(state.mFilename), "rw"); + out = new DrmOutputStream(drmClient, file, state.mMimeType); + outFd = file.getFD(); + } else { + out = new FileOutputStream(state.mFilename, true); + outFd = ((FileOutputStream) out).getFD(); + } + } catch (IOException e) { + throw new StopRequestException(STATUS_FILE_ERROR, e); + } + + // Start streaming data, periodically watch for pause/cancel + // commands and checking disk space as needed. + transferData(state, in, out); + + try { + if (out instanceof DrmOutputStream) { + ((DrmOutputStream) out).finish(); + } + } catch (IOException e) { + throw new StopRequestException(STATUS_FILE_ERROR, e); + } + + } finally { + if (drmClient != null) { + drmClient.release(); + } + + IoUtils.closeQuietly(in); + + try { + if (out != null) out.flush(); + if (outFd != null) outFd.sync(); + } catch (IOException e) { + } finally { + IoUtils.closeQuietly(out); + } + } } /** @@ -285,40 +436,38 @@ public class DownloadThread extends Thread { // checking connectivity will apply current policy mPolicyDirty = false; - int networkUsable = mInfo.checkCanUseNetwork(); - if (networkUsable != DownloadInfo.NETWORK_OK) { + final NetworkState networkUsable = mInfo.checkCanUseNetwork(); + if (networkUsable != NetworkState.OK) { int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; - if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) { + if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(true); - } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { + } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(false); } - throw new StopRequestException(status, - mInfo.getLogMessageForNetworkError(networkUsable)); + throw new StopRequestException(status, networkUsable.name()); } } /** - * Transfer as much data as possible from the HTTP response to the destination file. - * @param data buffer to use to read data - * @param entityStream stream for reading the HTTP response entity + * Transfer as much data as possible from the HTTP response to the + * destination file. */ - private void transferData( - State state, InnerState innerState, byte[] data, InputStream entityStream) + private void transferData(State state, InputStream in, OutputStream out) throws StopRequestException { + final byte data[] = new byte[Constants.BUFFER_SIZE]; for (;;) { - int bytesRead = readFromResponse(state, innerState, data, entityStream); + int bytesRead = readFromResponse(state, data, in); if (bytesRead == -1) { // success, end of stream already reached - handleEndOfStream(state, innerState); + handleEndOfStream(state); return; } state.mGotData = true; - writeDataToDestination(state, data, bytesRead); + writeDataToDestination(state, data, bytesRead, out); state.mCurrentBytes += bytesRead; - reportProgress(state, innerState); + reportProgress(state); if (Constants.LOGVV) { Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for " @@ -332,11 +481,10 @@ public class DownloadThread extends Thread { /** * Called after a successful completion to take any necessary action on the downloaded file. */ - private void finalizeDestinationFile(State state) throws StopRequestException { + private void finalizeDestinationFile(State state) { if (state.mFilename != null) { // make sure the file is readable FileUtils.setPermissions(state.mFilename, 0644, -1, -1); - syncDestination(state); } } @@ -345,11 +493,6 @@ public class DownloadThread extends Thread { * the downloaded file. */ private void cleanupDestination(State state, int finalStatus) { - if (mDrmConvertSession != null) { - finalStatus = mDrmConvertSession.close(state.mFilename); - } - - closeDestination(state); if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) { if (Constants.LOGVV) { Log.d(TAG, "cleanupDestination() deleting " + state.mFilename); @@ -360,53 +503,6 @@ public class DownloadThread extends Thread { } /** - * Sync the destination file to storage. - */ - private void syncDestination(State state) { - FileOutputStream downloadedFileStream = null; - try { - downloadedFileStream = new FileOutputStream(state.mFilename, true); - downloadedFileStream.getFD().sync(); - } catch (FileNotFoundException ex) { - Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); - } catch (SyncFailedException ex) { - Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); - } catch (IOException ex) { - Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); - } catch (RuntimeException ex) { - Log.w(Constants.TAG, "exception while syncing file: ", ex); - } finally { - if(downloadedFileStream != null) { - try { - downloadedFileStream.close(); - } catch (IOException ex) { - Log.w(Constants.TAG, "IOException while closing synced file: ", ex); - } catch (RuntimeException ex) { - Log.w(Constants.TAG, "exception while closing file: ", ex); - } - } - } - } - - /** - * Close the destination output stream. - */ - private void closeDestination(State state) { - try { - // close the file - if (state.mStream != null) { - state.mStream.close(); - state.mStream = null; - } - } catch (IOException ex) { - if (Constants.LOGV) { - Log.v(Constants.TAG, "exception when closing the file after download : " + ex); - } - // nothing can really be done if the file can't be closed - } - } - - /** * Check if the download has been paused or canceled, stopping the request appropriately if it * has been. */ @@ -430,7 +526,7 @@ public class DownloadThread extends Thread { /** * Report download progress through the database if necessary. */ - private void reportProgress(State state, InnerState innerState) { + private void reportProgress(State state) { final long now = SystemClock.elapsedRealtime(); final long sampleDelta = now - state.mSpeedSampleStart; @@ -444,10 +540,13 @@ public class DownloadThread extends Thread { state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4; } + // Only notify once we have a full sample window + if (state.mSpeedSampleStart != 0) { + mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed); + } + state.mSpeedSampleStart = now; state.mSpeedSampleBytes = state.mCurrentBytes; - - DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed); } if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP && @@ -465,37 +564,25 @@ public class DownloadThread extends Thread { * @param data buffer containing the data to write * @param bytesRead how many bytes to write from the buffer */ - private void writeDataToDestination(State state, byte[] data, int bytesRead) + private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out) throws StopRequestException { - for (;;) { + mStorageManager.verifySpaceBeforeWritingToFile( + mInfo.mDestination, state.mFilename, bytesRead); + + boolean forceVerified = false; + while (true) { try { - if (state.mStream == null) { - state.mStream = new FileOutputStream(state.mFilename, true); - } - mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename, - bytesRead); - if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) { - state.mStream.write(data, 0, bytesRead); - } else { - byte[] convertedData = mDrmConvertSession.convert(data, bytesRead); - if (convertedData != null) { - state.mStream.write(convertedData, 0, convertedData.length); - } else { - throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, - "Error converting drm data."); - } - } + out.write(data, 0, bytesRead); return; } catch (IOException ex) { - // couldn't write to file. are we out of space? check. - // TODO this check should only be done once. why is this being done - // in a while(true) loop (see the enclosing statement: for(;;) - if (state.mStream != null) { + // TODO: better differentiate between DRM and disk failures + if (!forceVerified) { + // couldn't write to file. are we out of space? check. mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead); - } - } finally { - if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) { - closeDestination(state); + forceVerified = true; + } else { + throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, + "Failed to write data: " + ex); } } } @@ -505,29 +592,30 @@ public class DownloadThread extends Thread { * Called when we've reached the end of the HTTP response stream, to update the database and * check for consistency. */ - private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException { + private void handleEndOfStream(State state) throws StopRequestException { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); - if (innerState.mHeaderContentLength == null) { + if (state.mContentLength == -1) { values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); } mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); - boolean lengthMismatched = (innerState.mHeaderContentLength != null) - && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength)); + final boolean lengthMismatched = (state.mContentLength != -1) + && (state.mCurrentBytes != state.mContentLength); if (lengthMismatched) { if (cannotResume(state)) { - throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, - "mismatched content length"); + throw new StopRequestException(STATUS_CANNOT_RESUME, + "mismatched content length; unable to resume"); } else { - throw new StopRequestException(getFinalStatusForHttpError(state), + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "closed socket before end of file"); } } } private boolean cannotResume(State state) { - return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null; + return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null) + || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType); } /** @@ -536,91 +624,51 @@ public class DownloadThread extends Thread { * @param entityStream stream for reading the HTTP response entity * @return the number of bytes actually read or -1 if the end of the stream has been reached */ - private int readFromResponse(State state, InnerState innerState, byte[] data, - InputStream entityStream) throws StopRequestException { + private int readFromResponse(State state, byte[] data, InputStream entityStream) + throws StopRequestException { try { return entityStream.read(data); } catch (IOException ex) { - logNetworkState(mInfo.mUid); + // TODO: handle stream errors the same as other retries + if ("unexpected end of stream".equals(ex.getMessage())) { + return -1; + } + ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); if (cannotResume(state)) { - String message = "while reading response: " + ex.toString() - + ", can't resume interrupted download with no ETag"; - throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, - message, ex); + throw new StopRequestException(STATUS_CANNOT_RESUME, + "Failed reading response: " + ex + "; unable to resume", ex); } else { - throw new StopRequestException(getFinalStatusForHttpError(state), - "while reading response: " + ex.toString(), ex); + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, + "Failed reading response: " + ex, ex); } } } /** - * Open a stream for the HTTP response entity, handling I/O errors. - * @return an InputStream to read the response entity + * Prepare target file based on given network response. Derives filename and + * target size as needed. */ - private InputStream openResponseEntity(State state, HttpResponse response) + private void processResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { - try { - return response.getEntity().getContent(); - } catch (IOException ex) { - logNetworkState(mInfo.mUid); - throw new StopRequestException(getFinalStatusForHttpError(state), - "while getting entity: " + ex.toString(), ex); - } - } + // TODO: fallocate the entire file if header gave us specific length - private void logNetworkState(int uid) { - if (Constants.LOGX) { - Log.i(Constants.TAG, - "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down")); - } - } - - /** - * Read HTTP response headers and take appropriate action, including setting up the destination - * file and updating the database. - */ - private void processResponseHeaders(State state, InnerState innerState, HttpResponse response) - throws StopRequestException { - if (state.mContinuingDownload) { - // ignore response headers on resume requests - return; - } - - readResponseHeaders(state, innerState, response); - if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) { - mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType); - if (mDrmConvertSession == null) { - throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype " - + state.mMimeType + " can not be converted."); - } - } + readResponseHeaders(state, conn); state.mFilename = Helpers.generateSaveFile( mContext, mInfo.mUri, mInfo.mHint, - innerState.mHeaderContentDisposition, - innerState.mHeaderContentLocation, + state.mContentDisposition, + state.mContentLocation, state.mMimeType, mInfo.mDestination, - (innerState.mHeaderContentLength != null) ? - Long.parseLong(innerState.mHeaderContentLength) : 0, - mInfo.mIsPublicApi, mStorageManager); - try { - state.mStream = new FileOutputStream(state.mFilename); - } catch (FileNotFoundException exc) { - throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, - "while opening destination file: " + exc.toString(), exc); - } - if (Constants.LOGV) { - Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename); - } + state.mContentLength, + mStorageManager); - updateDatabaseFromHeaders(state, innerState); + updateDatabaseFromHeaders(state); // check connectivity again now that we know the total size checkConnectivity(); } @@ -629,7 +677,7 @@ public class DownloadThread extends Thread { * Update necessary database fields based on values of HTTP response headers that have been * read. */ - private void updateDatabaseFromHeaders(State state, InnerState innerState) { + private void updateDatabaseFromHeaders(State state) { ContentValues values = new ContentValues(); values.put(Downloads.Impl._DATA, state.mFilename); if (state.mHeaderETag != null) { @@ -645,219 +693,48 @@ public class DownloadThread extends Thread { /** * Read headers from the HTTP response and store them into local state. */ - private void readResponseHeaders(State state, InnerState innerState, HttpResponse response) + private void readResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { - Header header = response.getFirstHeader("Content-Disposition"); - if (header != null) { - innerState.mHeaderContentDisposition = header.getValue(); - } - header = response.getFirstHeader("Content-Location"); - if (header != null) { - innerState.mHeaderContentLocation = header.getValue(); - } - if (state.mMimeType == null) { - header = response.getFirstHeader("Content-Type"); - if (header != null) { - state.mMimeType = Intent.normalizeMimeType(header.getValue()); - } - } - header = response.getFirstHeader("ETag"); - if (header != null) { - state.mHeaderETag = header.getValue(); - } - String headerTransferEncoding = null; - header = response.getFirstHeader("Transfer-Encoding"); - if (header != null) { - headerTransferEncoding = header.getValue(); - } - if (headerTransferEncoding == null) { - header = response.getFirstHeader("Content-Length"); - if (header != null) { - innerState.mHeaderContentLength = header.getValue(); - state.mTotalBytes = mInfo.mTotalBytes = - Long.parseLong(innerState.mHeaderContentLength); - } - } else { - // Ignore content-length with transfer-encoding - 2616 4.4 3 - if (Constants.LOGVV) { - Log.v(Constants.TAG, - "ignoring content-length because of xfer-encoding"); - } - } - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Content-Disposition: " + - innerState.mHeaderContentDisposition); - Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength); - Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation); - Log.v(Constants.TAG, "Content-Type: " + state.mMimeType); - Log.v(Constants.TAG, "ETag: " + state.mHeaderETag); - Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); - } - - boolean noSizeInfo = innerState.mHeaderContentLength == null - && (headerTransferEncoding == null - || !headerTransferEncoding.equalsIgnoreCase("chunked")); - if (!mInfo.mNoIntegrity && noSizeInfo) { - throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, - "can't know size of download, giving up"); - } - } + state.mContentDisposition = conn.getHeaderField("Content-Disposition"); + state.mContentLocation = conn.getHeaderField("Content-Location"); - /** - * Check the HTTP response status and handle anything unusual (e.g. not 200/206). - */ - private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response) - throws StopRequestException, RetryDownload { - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { - handleServiceUnavailable(state, response); - } - if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) { - handleRedirect(state, response, statusCode); + if (state.mMimeType == null) { + state.mMimeType = Intent.normalizeMimeType(conn.getContentType()); } - if (Constants.LOGV) { - Log.i(Constants.TAG, "recevd_status = " + statusCode + - ", mContinuingDownload = " + state.mContinuingDownload); - } - int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS; - if (statusCode != expectedStatus) { - handleOtherStatus(state, innerState, statusCode); - } - } + state.mHeaderETag = conn.getHeaderField("ETag"); - /** - * Handle a status that we don't know how to deal with properly. - */ - private void handleOtherStatus(State state, InnerState innerState, int statusCode) - throws StopRequestException { - if (statusCode == 416) { - // range request failed. it should never fail. - throw new IllegalStateException("Http Range request failure: totalBytes = " + - state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes); - } - int finalStatus; - if (Downloads.Impl.isStatusError(statusCode)) { - finalStatus = statusCode; - } else if (statusCode >= 300 && statusCode < 400) { - finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT; - } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) { - finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME; + final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); + if (transferEncoding == null) { + state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); } else { - finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; + Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined"); + state.mContentLength = -1; } - throw new StopRequestException(finalStatus, "http error " + - statusCode + ", mContinuingDownload: " + state.mContinuingDownload); - } - /** - * Handle a 3xx redirect status. - */ - private void handleRedirect(State state, HttpResponse response, int statusCode) - throws StopRequestException, RetryDownload { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP redirect " + statusCode); - } - if (state.mRedirectCount >= Constants.MAX_REDIRECTS) { - throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, - "too many redirects"); - } - Header header = response.getFirstHeader("Location"); - if (header == null) { - return; - } - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Location :" + header.getValue()); - } - - String newUri; - try { - newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString(); - } catch(URISyntaxException ex) { - if (Constants.LOGV) { - Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue() - + " for " + mInfo.mUri); - } - throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, - "Couldn't resolve redirect URI"); - } - ++state.mRedirectCount; - state.mRequestUri = newUri; - if (statusCode == 301 || statusCode == 303) { - // use the new URI for all future requests (should a retry/resume be necessary) - state.mNewUri = newUri; - } - throw new RetryDownload(); - } + state.mTotalBytes = state.mContentLength; + mInfo.mTotalBytes = state.mContentLength; - /** - * Handle a 503 Service Unavailable status by processing the Retry-After header. - */ - private void handleServiceUnavailable(State state, HttpResponse response) - throws StopRequestException { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP response code 503"); - } - state.mCountRetry = true; - Header header = response.getFirstHeader("Retry-After"); - if (header != null) { - try { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Retry-After :" + header.getValue()); - } - state.mRetryAfter = Integer.parseInt(header.getValue()); - if (state.mRetryAfter < 0) { - state.mRetryAfter = 0; - } else { - if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { - state.mRetryAfter = Constants.MIN_RETRY_AFTER; - } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { - state.mRetryAfter = Constants.MAX_RETRY_AFTER; - } - state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); - state.mRetryAfter *= 1000; - } - } catch (NumberFormatException ex) { - // ignored - retryAfter stays 0 in this case. - } - } - throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY, - "got 503 Service Unavailable, will retry later"); - } - - /** - * Send the request to the server, handling any I/O exceptions. - */ - private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request) - throws StopRequestException { - try { - return client.execute(request); - } catch (IllegalArgumentException ex) { - throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, - "while trying to execute request: " + ex.toString(), ex); - } catch (IOException ex) { - logNetworkState(mInfo.mUid); - throw new StopRequestException(getFinalStatusForHttpError(state), - "while trying to execute request: " + ex.toString(), ex); + final boolean noSizeInfo = state.mContentLength == -1 + && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")); + if (!mInfo.mNoIntegrity && noSizeInfo) { + throw new StopRequestException(STATUS_CANNOT_RESUME, + "can't know size of download, giving up"); } } - private int getFinalStatusForHttpError(State state) { - int networkUsable = mInfo.checkCanUseNetwork(); - if (networkUsable != DownloadInfo.NETWORK_OK) { - switch (networkUsable) { - case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE: - case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: - return Downloads.Impl.STATUS_QUEUED_FOR_WIFI; - default: - return Downloads.Impl.STATUS_WAITING_FOR_NETWORK; - } - } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) { - state.mCountRetry = true; - return Downloads.Impl.STATUS_WAITING_TO_RETRY; + private void parseRetryAfterHeaders(State state, HttpURLConnection conn) { + state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1); + if (state.mRetryAfter < 0) { + state.mRetryAfter = 0; } else { - Log.w(Constants.TAG, "reached max retries for " + mInfo.mId); - return Downloads.Impl.STATUS_HTTP_DATA_ERROR; + if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { + state.mRetryAfter = Constants.MIN_RETRY_AFTER; + } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { + state.mRetryAfter = Constants.MAX_RETRY_AFTER; + } + state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); + state.mRetryAfter *= 1000; } } @@ -865,8 +742,7 @@ public class DownloadThread extends Thread { * Prepare the destination file to receive data. If the file already exists, we'll set up * appropriately for resumption. */ - private void setupDestinationFile(State state, InnerState innerState) - throws StopRequestException { + private void setupDestinationFile(State state) throws StopRequestException { if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download if (Constants.LOGV) { Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId + @@ -913,15 +789,9 @@ public class DownloadThread extends Thread { Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + ", and starting with file of length: " + fileLength); } - try { - state.mStream = new FileOutputStream(state.mFilename, true); - } catch (FileNotFoundException exc) { - throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, - "while opening destination for resuming: " + exc.toString(), exc); - } state.mCurrentBytes = (int) fileLength; if (mInfo.mTotalBytes != -1) { - innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); + state.mContentLength = mInfo.mTotalBytes; } state.mHeaderETag = mInfo.mETag; state.mContinuingDownload = true; @@ -933,30 +803,30 @@ public class DownloadThread extends Thread { } } } - - if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) { - closeDestination(state); - } } /** * Add custom headers for this download to the HTTP request. */ - private void addRequestHeaders(State state, HttpGet request) { + private void addRequestHeaders(State state, HttpURLConnection conn) { for (Pair<String, String> header : mInfo.getHeaders()) { - request.addHeader(header.first, header.second); + conn.addRequestProperty(header.first, header.second); } + // Only splice in user agent when not already defined + if (conn.getRequestProperty("User-Agent") == null) { + conn.addRequestProperty("User-Agent", userAgent()); + } + + // Defeat transparent gzip compression, since it doesn't allow us to + // easily resume partial downloads. + conn.setRequestProperty("Accept-Encoding", "identity"); + if (state.mContinuingDownload) { if (state.mHeaderETag != null) { - request.addHeader("If-Match", state.mHeaderETag); - } - request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-"); - if (Constants.LOGV) { - Log.i(Constants.TAG, "Adding Range header: " + - "bytes=" + state.mCurrentBytes + "-"); - Log.i(Constants.TAG, " totalBytes = " + state.mTotalBytes); + conn.addRequestProperty("If-Match", state.mHeaderETag); } + conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-"); } } @@ -964,35 +834,27 @@ public class DownloadThread extends Thread { * Stores information about the completed download, and notifies the initiating application. */ private void notifyDownloadCompleted( - int status, boolean countRetry, int retryAfter, boolean gotData, - String filename, String uri, String mimeType, String errorMsg) { - notifyThroughDatabase( - status, countRetry, retryAfter, gotData, filename, uri, mimeType, - errorMsg); - if (Downloads.Impl.isStatusCompleted(status)) { + State state, int finalStatus, String errorMsg, int numFailed) { + notifyThroughDatabase(state, finalStatus, errorMsg, numFailed); + if (Downloads.Impl.isStatusCompleted(finalStatus)) { mInfo.sendIntentIfRequested(); } } private void notifyThroughDatabase( - int status, boolean countRetry, int retryAfter, boolean gotData, - String filename, String uri, String mimeType, String errorMsg) { + State state, int finalStatus, String errorMsg, int numFailed) { ContentValues values = new ContentValues(); - values.put(Downloads.Impl.COLUMN_STATUS, status); - values.put(Downloads.Impl._DATA, filename); - if (uri != null) { - values.put(Downloads.Impl.COLUMN_URI, uri); - } - values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType); + values.put(Downloads.Impl.COLUMN_STATUS, finalStatus); + values.put(Downloads.Impl._DATA, state.mFilename); + values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); - values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter); - if (!countRetry) { - values.put(Constants.FAILED_CONNECTIONS, 0); - } else if (gotData) { - values.put(Constants.FAILED_CONNECTIONS, 1); - } else { - values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1); + values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed); + values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter); + + if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) { + values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri); } + // save the error message. could be useful to developers. if (!TextUtils.isEmpty(errorMsg)) { values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg); @@ -1021,4 +883,27 @@ public class DownloadThread extends Thread { mPolicyDirty = true; } }; + + public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { + try { + return Long.parseLong(conn.getHeaderField(field)); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Return if given status is eligible to be treated as + * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}. + */ + public static boolean isStatusRetryable(int status) { + switch (status) { + case STATUS_HTTP_DATA_ERROR: + case HTTP_UNAVAILABLE: + case HTTP_INTERNAL_ERROR: + return true; + default: + return false; + } + } } |