/* * 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; import java.util.Locale; /** * 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 = sanitizeMimeType(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 = sanitizeMimeType(header.getValue()); } } 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); } /** * Clean up a mimeType string so it can be used to dispatch an intent to * view a downloaded asset. * @param mimeType either null or one or more mime types (semi colon separated). * @return null if mimeType was null. Otherwise a string which represents a * single mimetype in lowercase and with surrounding whitespaces trimmed. */ private String sanitizeMimeType(String mimeType) { try { mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH); final int semicolonIndex = mimeType.indexOf(';'); if (semicolonIndex != -1) { mimeType = mimeType.substring(0, semicolonIndex); } return mimeType; } catch (NullPointerException npe) { return null; } } }