diff options
Diffstat (limited to 'src/com/android/providers/downloads/DownloadThread.java')
-rw-r--r-- | src/com/android/providers/downloads/DownloadThread.java | 710 |
1 files changed, 710 insertions, 0 deletions
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java new file mode 100644 index 00000000..923e36d1 --- /dev/null +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -0,0 +1,710 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.providers.downloads; + +import org.apache.http.client.methods.AbortableHttpRequest; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.HttpClient; +import org.apache.http.entity.StringEntity; +import org.apache.http.Header; +import org.apache.http.HttpResponse; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.drm.mobile1.DrmRawContent; +import android.net.http.AndroidHttpClient; +import android.net.Uri; +import android.os.FileUtils; +import android.os.PowerManager; +import android.os.Process; +import android.provider.Downloads; +import android.provider.DrmStore; +import android.util.Config; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; + +/** + * Runs an actual download + */ +public class DownloadThread extends Thread { + + private Context mContext; + private DownloadInfo mInfo; + + public DownloadThread(Context context, DownloadInfo info) { + mContext = context; + mInfo = info; + } + + /** + * Returns the user agent provided by the initiating app, or use the default one + */ + private String userAgent() { + String userAgent = mInfo.userAgent; + if (userAgent != null) { + } + if (userAgent == null) { + userAgent = Constants.DEFAULT_USER_AGENT; + } + return userAgent; + } + + /** + * Executes the download in a separate thread + */ + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + int finalStatus = Downloads.STATUS_UNKNOWN_ERROR; + boolean countRetry = false; + int retryAfter = 0; + int redirectCount = mInfo.redirectCount; + String newUri = null; + boolean gotData = false; + String filename = null; + String mimeType = mInfo.mimetype; + FileOutputStream stream = null; + AndroidHttpClient client = null; + PowerManager.WakeLock wakeLock = null; + Uri contentUri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id); + + try { + boolean continuingDownload = false; + String headerAcceptRanges = null; + String headerContentDisposition = null; + String headerContentLength = null; + String headerContentLocation = null; + String headerETag = null; + String headerTransferEncoding = null; + + byte data[] = new byte[Constants.BUFFER_SIZE]; + + int bytesSoFar = 0; + + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); + wakeLock.acquire(); + + filename = mInfo.filename; + if (filename != null) { + if (!Helpers.isFilenameValid(filename)) { + finalStatus = Downloads.STATUS_FILE_ERROR; + notifyDownloadCompleted( + finalStatus, false, 0, 0, false, filename, null, mInfo.mimetype); + return; + } + // We're resuming a download that got interrupted + File f = new File(filename); + if (f.exists()) { + long fileLength = f.length(); + if (fileLength == 0) { + // The download hadn't actually started, we can restart from scratch + f.delete(); + filename = null; + } else if (mInfo.etag == null && !mInfo.noIntegrity) { + // Tough luck, that's not a resumable download + if (Config.LOGD) { + Log.d(Constants.TAG, + "can't resume interrupted non-resumable download"); + } + f.delete(); + finalStatus = Downloads.STATUS_PRECONDITION_FAILED; + notifyDownloadCompleted( + finalStatus, false, 0, 0, false, filename, null, mInfo.mimetype); + return; + } else { + // All right, we'll be able to resume this download + stream = new FileOutputStream(filename, true); + bytesSoFar = (int) fileLength; + if (mInfo.totalBytes != -1) { + headerContentLength = Integer.toString(mInfo.totalBytes); + } + headerETag = mInfo.etag; + continuingDownload = true; + } + } + } + + int bytesNotified = bytesSoFar; + // starting with MIN_VALUE means that the first write will commit + // progress to the database + long timeLastNotification = 0; + + client = AndroidHttpClient.newInstance(userAgent()); + + if (stream != null && mInfo.destination == Downloads.DESTINATION_EXTERNAL + && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING + .equalsIgnoreCase(mimeType)) { + try { + stream.close(); + stream = null; + } catch (IOException ex) { + if (Constants.LOGV) { + Log.v(Constants.TAG, "exception when closing the file before download : " + + ex); + } + // nothing can really be done if the file can't be closed + } + } + + /* + * This loop is run once for every individual HTTP request that gets sent. + * The very first HTTP request is a "virgin" request, while every subsequent + * request is done with the original ETag and a byte-range. + */ +http_request_loop: + while (true) { + // Prepares the request and fires it. + HttpGet request = new HttpGet(mInfo.uri); + + if (Constants.LOGV) { + Log.v(Constants.TAG, "initiating download for " + mInfo.uri); + } + + if (mInfo.cookies != null) { + request.addHeader("Cookie", mInfo.cookies); + } + if (mInfo.referer != null) { + request.addHeader("Referer", mInfo.referer); + } + if (continuingDownload) { + if (headerETag != null) { + request.addHeader("If-Match", headerETag); + } + request.addHeader("Range", "bytes=" + bytesSoFar + "-"); + } + + HttpResponse response; + try { + response = client.execute(request); + } catch (IllegalArgumentException ex) { + if (Constants.LOGV) { + Log.d(Constants.TAG, "Arg exception trying to execute request for " + + mInfo.uri + " : " + ex); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "Arg exception trying to execute request for " + + mInfo.id + " : " + ex); + } + finalStatus = Downloads.STATUS_BAD_REQUEST; + request.abort(); + break http_request_loop; + } catch (IOException ex) { + if (!Helpers.isNetworkAvailable(mContext)) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + } else if (mInfo.numFailed < Constants.MAX_RETRIES) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + countRetry = true; + } else { + if (Constants.LOGV) { + Log.d(Constants.TAG, "IOException trying to execute request for " + + mInfo.uri + " : " + ex); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "IOException trying to execute request for " + + mInfo.id + " : " + ex); + } + finalStatus = Downloads.STATUS_HTTP_DATA_ERROR; + } + request.abort(); + break http_request_loop; + } + + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 503 && mInfo.numFailed < Constants.MAX_RETRIES) { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "got HTTP response code 503"); + } + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + countRetry = true; + Header header = response.getFirstHeader("Retry-After"); + if (header != null) { + try { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "Retry-After :" + header.getValue()); + } + retryAfter = Integer.parseInt(header.getValue()); + if (retryAfter < 0) { + retryAfter = 0; + } else { + if (retryAfter < Constants.MIN_RETRY_AFTER) { + retryAfter = Constants.MIN_RETRY_AFTER; + } else if (retryAfter > Constants.MAX_RETRY_AFTER) { + retryAfter = Constants.MAX_RETRY_AFTER; + } + retryAfter += Helpers.rnd.nextInt(Constants.MIN_RETRY_AFTER + 1); + retryAfter *= 1000; + } + } catch (NumberFormatException ex) { + // ignored - retryAfter stays 0 in this case. + } + } + request.abort(); + break http_request_loop; + } + if (statusCode == 301 || + statusCode == 302 || + statusCode == 303 || + statusCode == 307) { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "got HTTP redirect " + statusCode); + } + if (redirectCount >= Constants.MAX_REDIRECTS) { + if (Constants.LOGV) { + Log.d(Constants.TAG, "too many redirects for download " + mInfo.id + + " at " + mInfo.uri); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "too many redirects for download " + mInfo.id); + } + finalStatus = Downloads.STATUS_TOO_MANY_REDIRECTS; + request.abort(); + break http_request_loop; + } + Header header = response.getFirstHeader("Location"); + if (header != null) { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "Location :" + header.getValue()); + } + newUri = new URI(mInfo.uri).resolve(new URI(header.getValue())).toString(); + ++redirectCount; + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + request.abort(); + break http_request_loop; + } + } + if ((!continuingDownload && statusCode != Downloads.STATUS_SUCCESS) + || (continuingDownload && statusCode != 206)) { + if (Constants.LOGV) { + Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.uri); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "http error " + statusCode + " for download " + + mInfo.id); + } + if (Downloads.isStatusError(statusCode)) { + finalStatus = statusCode; + } else if (statusCode >= 300 && statusCode < 400) { + finalStatus = Downloads.STATUS_UNHANDLED_REDIRECT; + } else if (continuingDownload && statusCode == Downloads.STATUS_SUCCESS) { + finalStatus = Downloads.STATUS_PRECONDITION_FAILED; + } else { + finalStatus = Downloads.STATUS_UNHANDLED_HTTP_CODE; + } + request.abort(); + break http_request_loop; + } else { + // Handles the response, saves the file + if (Constants.LOGV) { + Log.v(Constants.TAG, "received response for " + mInfo.uri); + } + + if (!continuingDownload) { + Header header = response.getFirstHeader("Accept-Ranges"); + if (header != null) { + headerAcceptRanges = header.getValue(); + } + header = response.getFirstHeader("Content-Disposition"); + if (header != null) { + headerContentDisposition = header.getValue(); + } + header = response.getFirstHeader("Content-Location"); + if (header != null) { + headerContentLocation = header.getValue(); + } + if (mimeType == null) { + header = response.getFirstHeader("Content-Type"); + if (header != null) { + mimeType = header.getValue(); + final int semicolonIndex = mimeType.indexOf(';'); + if (semicolonIndex != -1) { + mimeType = mimeType.substring(0, semicolonIndex); + } + } + } + header = response.getFirstHeader("ETag"); + if (header != null) { + headerETag = header.getValue(); + } + header = response.getFirstHeader("Transfer-Encoding"); + if (header != null) { + headerTransferEncoding = header.getValue(); + } + if (headerTransferEncoding == null) { + header = response.getFirstHeader("Content-Length"); + if (header != null) { + headerContentLength = header.getValue(); + } + } 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, "Accept-Ranges: " + headerAcceptRanges); + Log.v(Constants.TAG, "Content-Disposition: " + + headerContentDisposition); + Log.v(Constants.TAG, "Content-Length: " + headerContentLength); + Log.v(Constants.TAG, "Content-Location: " + headerContentLocation); + Log.v(Constants.TAG, "Content-Type: " + mimeType); + Log.v(Constants.TAG, "ETag: " + headerETag); + Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); + } + + if (!mInfo.noIntegrity && headerContentLength == null && + (headerTransferEncoding == null + || !headerTransferEncoding.equalsIgnoreCase("chunked")) + ) { + if (Config.LOGD) { + Log.d(Constants.TAG, "can't know size of download, giving up"); + } + finalStatus = Downloads.STATUS_LENGTH_REQUIRED; + request.abort(); + break http_request_loop; + } + + DownloadFileInfo fileInfo = Helpers.generateSaveFile( + mContext, + mInfo.uri, + mInfo.hint, + headerContentDisposition, + headerContentLocation, + mimeType, + mInfo.destination, + (headerContentLength != null) ? + Integer.parseInt(headerContentLength) : 0); + if (fileInfo.filename == null) { + finalStatus = fileInfo.status; + request.abort(); + break http_request_loop; + } + filename = fileInfo.filename; + stream = fileInfo.stream; + if (Constants.LOGV) { + Log.v(Constants.TAG, "writing " + mInfo.uri + " to " + filename); + } + + ContentValues values = new ContentValues(); + values.put(Downloads._DATA, filename); + if (headerETag != null) { + values.put(Constants.ETAG, headerETag); + } + if (mimeType != null) { + values.put(Downloads.MIMETYPE, mimeType); + } + int contentLength = -1; + if (headerContentLength != null) { + contentLength = Integer.parseInt(headerContentLength); + } + values.put(Downloads.TOTAL_BYTES, contentLength); + mContext.getContentResolver().update(contentUri, values, null, null); + } + + InputStream entityStream; + try { + entityStream = response.getEntity().getContent(); + } catch (IOException ex) { + if (!Helpers.isNetworkAvailable(mContext)) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + } else if (mInfo.numFailed < Constants.MAX_RETRIES) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + countRetry = true; + } else { + if (Constants.LOGV) { + Log.d(Constants.TAG, "IOException getting entity for " + mInfo.uri + + " : " + ex); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "IOException getting entity for download " + + mInfo.id + " : " + ex); + } + finalStatus = Downloads.STATUS_HTTP_DATA_ERROR; + } + request.abort(); + break http_request_loop; + } + for (;;) { + int bytesRead; + try { + bytesRead = entityStream.read(data); + } catch (IOException ex) { + ContentValues values = new ContentValues(); + values.put(Downloads.CURRENT_BYTES, bytesSoFar); + mContext.getContentResolver().update(contentUri, values, null, null); + if (!mInfo.noIntegrity && headerETag == null) { + if (Constants.LOGV) { + Log.v(Constants.TAG, "download IOException for " + mInfo.uri + + " : " + ex); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "download IOException for download " + + mInfo.id + " : " + ex); + } + if (Config.LOGD) { + Log.d(Constants.TAG, + "can't resume interrupted download with no ETag"); + } + finalStatus = Downloads.STATUS_PRECONDITION_FAILED; + } else if (!Helpers.isNetworkAvailable(mContext)) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + } else if (mInfo.numFailed < Constants.MAX_RETRIES) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + countRetry = true; + } else { + if (Constants.LOGV) { + Log.v(Constants.TAG, "download IOException for " + mInfo.uri + + " : " + ex); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "download IOException for download " + + mInfo.id + " : " + ex); + } + finalStatus = Downloads.STATUS_HTTP_DATA_ERROR; + } + request.abort(); + break http_request_loop; + } + if (bytesRead == -1) { // success + ContentValues values = new ContentValues(); + values.put(Downloads.CURRENT_BYTES, bytesSoFar); + if (headerContentLength == null) { + values.put(Downloads.TOTAL_BYTES, bytesSoFar); + } + mContext.getContentResolver().update(contentUri, values, null, null); + if ((headerContentLength != null) + && (bytesSoFar + != Integer.parseInt(headerContentLength))) { + if (!mInfo.noIntegrity && headerETag == null) { + if (Constants.LOGV) { + Log.d(Constants.TAG, "mismatched content length " + + mInfo.uri); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "mismatched content length for " + + mInfo.id); + } + finalStatus = Downloads.STATUS_LENGTH_REQUIRED; + } else if (!Helpers.isNetworkAvailable(mContext)) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + } else if (mInfo.numFailed < Constants.MAX_RETRIES) { + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + countRetry = true; + } else { + if (Constants.LOGV) { + Log.v(Constants.TAG, "closed socket for " + mInfo.uri); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "closed socket for download " + + mInfo.id); + } + finalStatus = Downloads.STATUS_HTTP_DATA_ERROR; + } + break http_request_loop; + } + break; + } + gotData = true; + for (;;) { + try { + if (stream == null) { + stream = new FileOutputStream(filename, true); + } + stream.write(data, 0, bytesRead); + if (mInfo.destination == Downloads.DESTINATION_EXTERNAL + && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING + .equalsIgnoreCase(mimeType)) { + try { + stream.close(); + stream = null; + } catch (IOException ex) { + if (Constants.LOGV) { + Log.v(Constants.TAG, + "exception when closing the file " + + "during download : " + ex); + } + // nothing can really be done if the file can't be closed + } + } + break; + } catch (IOException ex) { + if (!Helpers.discardPurgeableFiles( + mContext, Constants.BUFFER_SIZE)) { + finalStatus = Downloads.STATUS_FILE_ERROR; + break http_request_loop; + } + } + } + bytesSoFar += bytesRead; + long now = System.currentTimeMillis(); + if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP + && now - timeLastNotification + > Constants.MIN_PROGRESS_TIME) { + ContentValues values = new ContentValues(); + values.put(Downloads.CURRENT_BYTES, bytesSoFar); + mContext.getContentResolver().update( + contentUri, values, null, null); + bytesNotified = bytesSoFar; + timeLastNotification = now; + } + + if (Constants.LOGVV) { + Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.uri); + } + synchronized(mInfo) { + if (mInfo.control == Downloads.CONTROL_PAUSED) { + if (Constants.LOGV) { + Log.v(Constants.TAG, "paused " + mInfo.uri); + } + finalStatus = Downloads.STATUS_RUNNING_PAUSED; + request.abort(); + break http_request_loop; + } + } + if (mInfo.status == Downloads.STATUS_CANCELED) { + if (Constants.LOGV) { + Log.d(Constants.TAG, "canceled " + mInfo.uri); + } else if (Config.LOGD) { + // Log.d(Constants.TAG, "canceled id " + mInfo.id); + } + finalStatus = Downloads.STATUS_CANCELED; + break http_request_loop; + } + } + if (Constants.LOGV) { + Log.v(Constants.TAG, "download completed for " + mInfo.uri); + } + finalStatus = Downloads.STATUS_SUCCESS; + } + break; + } + } catch (FileNotFoundException ex) { + if (Config.LOGD) { + Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " + ex); + } + finalStatus = Downloads.STATUS_FILE_ERROR; + // falls through to the code that reports an error + } catch (Exception ex) { //sometimes the socket code throws unchecked exceptions + if (Constants.LOGV) { + Log.d(Constants.TAG, "Exception for " + mInfo.uri, ex); + } else if (Config.LOGD) { + Log.d(Constants.TAG, "Exception for id " + mInfo.id, ex); + } + finalStatus = Downloads.STATUS_UNKNOWN_ERROR; + // falls through to the code that reports an error + } finally { + mInfo.hasActiveThread = false; + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + if (client != null) { + client.close(); + client = null; + } + try { + // close the file + if (stream != null) { + stream.close(); + } + } 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 + } + if (filename != null) { + // if the download wasn't successful, delete the file + if (Downloads.isStatusError(finalStatus)) { + new File(filename).delete(); + filename = null; + } else if (Downloads.isStatusSuccess(finalStatus) && + DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING + .equalsIgnoreCase(mimeType)) { + // transfer the file to the DRM content provider + File file = new File(filename); + Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null); + if (item == null) { + Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider"); + finalStatus = Downloads.STATUS_UNKNOWN_ERROR; + } else { + filename = item.getDataString(); + mimeType = item.getType(); + } + + file.delete(); + } else if (Downloads.isStatusSuccess(finalStatus)) { + // make sure the file is readable + FileUtils.setPermissions(filename, 0644, -1, -1); + } + } + notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount, + gotData, filename, newUri, mimeType); + } + } + + /** + * Stores information about the completed download, and notifies the initiating application. + */ + private void notifyDownloadCompleted( + int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, + String filename, String uri, String mimeType) { + notifyThroughDatabase( + status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType); + if (Downloads.isStatusCompleted(status)) { + notifyThroughIntent(); + } + } + + private void notifyThroughDatabase( + int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, + String filename, String uri, String mimeType) { + ContentValues values = new ContentValues(); + values.put(Downloads.STATUS, status); + values.put(Downloads._DATA, filename); + if (uri != null) { + values.put(Downloads.URI, uri); + } + values.put(Downloads.MIMETYPE, mimeType); + values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis()); + values.put(Constants.RETRY_AFTER___REDIRECT_COUNT, retryAfter + (redirectCount << 28)); + if (!countRetry) { + values.put(Constants.FAILED_CONNECTIONS, 0); + } else if (gotData) { + values.put(Constants.FAILED_CONNECTIONS, 1); + } else { + values.put(Constants.FAILED_CONNECTIONS, mInfo.numFailed + 1); + } + + mContext.getContentResolver().update( + ContentUris.withAppendedId(Downloads.CONTENT_URI, mInfo.id), values, null, null); + } + + /** + * Notifies the initiating app if it requested it. That way, it can know that the + * download completed even if it's not actively watching the cursor. + */ + private void notifyThroughIntent() { + Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id); + mInfo.sendIntentIfRequested(uri, mContext); + } + +} |