/* * 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 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_SUCCESS; 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.TrafficStats; import android.os.FileUtils; import android.os.PowerManager; import android.os.Process; import android.os.SystemClock; import android.os.WorkSource; import android.provider.Downloads; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.providers.downloads.DownloadInfo.NetworkState; import libcore.io.IoUtils; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; /** * Task which executes a given {@link DownloadInfo}: making network requests, * persisting data to disk, and updating {@link DownloadProvider}. */ 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 final DownloadNotifier mNotifier; private volatile boolean mPolicyDirty; // Add for carrier feature - download breakpoint continuing. // Support continuing download after the download is broken // although HTTP Server doesn't contain etag in its response. private final static String QRD_ETAG = "qrd_magic_etag"; public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, StorageManager storageManager, DownloadNotifier notifier) { mContext = context; mSystemFacade = systemFacade; mInfo = info; mStorageManager = storageManager; mNotifier = notifier; } /** * Returns the user agent provided by the initiating app, or use the default one */ private String userAgent() { String userAgent = mInfo.mUserAgent; if (userAgent == null) { userAgent = Constants.DEFAULT_USER_AGENT; } return userAgent; } /** * State for the entire run() method. */ static class State { public String mFilename; public String mMimeType; public int mRetryAfter = 0; public boolean mGotData = false; public String mRequestUri; public long mTotalBytes = -1; public long mCurrentBytes = 0; public String mHeaderETag; 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; /** Time when current sample started. */ public long mSpeedSampleStart; /** 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; mFilename = info.mFileName; mTotalBytes = info.mTotalBytes; mCurrentBytes = info.mCurrentBytes; } public void resetBeforeExecute() { // Reset any state from previous execution mContentLength = -1; mContentDisposition = null; mContentLocation = null; mRedirectionCount = 0; } } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); try { runInternal(); } finally { mNotifier.notifyDownloadSpeed(mInfo.mId, 0); } } private void runInternal() { // Skip when download already marked as finished; this download was // probably started again while racing with UpdateThread. if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId) == Downloads.Impl.STATUS_SUCCESS) { Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping"); return; } State state = new State(mInfo); PowerManager.WakeLock wakeLock = null; int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; int numFailed = mInfo.mNumFailed; String errorMsg = null; final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext); final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); try { wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); wakeLock.setWorkSource(new WorkSource(mInfo.mUid)); wakeLock.acquire(); // while performing download, register for rules updates netPolicy.registerListener(mPolicyListener); Log.i(Constants.TAG, "Download " + mInfo.mId + " starting"); // 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. TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); TrafficStats.setThreadStatsUid(mInfo.mUid); 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); } executeDownload(state); finalizeDestinationFile(state); finalStatus = Downloads.Impl.STATUS_SUCCESS; } catch (StopRequestException error) { // remove the cause before printing, in case it contains PII errorMsg = error.getMessage(); String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg; Log.w(Constants.TAG, msg); if (Constants.LOGV) { Log.w(Constants.TAG, msg, error); } 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) { errorMsg = ex.getMessage(); String msg = "Exception for id " + mInfo.mId + ": " + errorMsg; Log.w(Constants.TAG, msg, ex); finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; // falls through to the code that reports an error } finally { if (finalStatus == STATUS_SUCCESS) { TrafficStats.incrementOperationCount(1); } TrafficStats.clearThreadStatsTag(); TrafficStats.clearThreadStatsUid(); cleanupDestination(state, finalStatus); notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed); Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status " + Downloads.Impl.statusToString(finalStatus)); netPolicy.unregisterListener(mPolicyListener); if (wakeLock != null) { wakeLock.release(); wakeLock = null; } } mStorageManager.incrementNumDownloadsSoFar(); } /** * 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) throws StopRequestException { state.resetBeforeExecute(); setupDestinationFile(state); // skip when already finished; remove after fixing race in 5217390 if (state.mCurrentBytes == state.mTotalBytes) { Log.i(Constants.TAG, "Skipping initiating request for download " + mInfo.mId + "; already completed"); return; } 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); } finally { if (conn != null) conn.disconnect(); } } 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); } } } /** * Check if current connectivity is valid for this request. */ private void checkConnectivity() throws StopRequestException { // checking connectivity will apply current policy mPolicyDirty = false; final NetworkState networkUsable = mInfo.checkCanUseNetwork(); if (networkUsable != NetworkState.OK) { int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(true); } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(false); } throw new StopRequestException(status, networkUsable.name()); } } /** * Transfer as much data as possible from the HTTP response to the * destination file. */ private void transferData(State state, InputStream in, OutputStream out) throws StopRequestException { final byte data[] = new byte[Constants.BUFFER_SIZE]; for (;;) { int bytesRead = readFromResponse(state, data, in); if (bytesRead == -1) { // success, end of stream already reached handleEndOfStream(state); return; } state.mGotData = true; writeDataToDestination(state, data, bytesRead, out); state.mCurrentBytes += bytesRead; reportProgress(state); if (Constants.LOGVV) { Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for " + mInfo.mUri); } checkPausedOrCanceled(state); } } /** * Called after a successful completion to take any necessary action on the downloaded file. */ private void finalizeDestinationFile(State state) { if (state.mFilename != null) { // make sure the file is readable FileUtils.setPermissions(state.mFilename, 0644, -1, -1); } } /** * Called just before the thread finishes, regardless of status, to take any necessary action on * the downloaded file. */ private void cleanupDestination(State state, int finalStatus) { if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) { if (Constants.LOGVV) { Log.d(TAG, "cleanupDestination() deleting " + state.mFilename); } new File(state.mFilename).delete(); state.mFilename = null; } } /** * Check if the download has been paused or canceled, stopping the request appropriately if it * has been. */ private void checkPausedOrCanceled(State state) throws StopRequestException { synchronized (mInfo) { if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { throw new StopRequestException( Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); } if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) { throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); } if (mInfo.mStatus == Downloads.Impl.STATUS_PAUSED_BY_MANUAL) { // user pauses the download by manual, here send exception and stop data transfer. throw new StopRequestException(Downloads.Impl.STATUS_PAUSED_BY_MANUAL, "download paused by manual"); } } // if policy has been changed, trigger connectivity check if (mPolicyDirty) { checkConnectivity(); } } /** * Report download progress through the database if necessary. */ private void reportProgress(State state) { final long now = SystemClock.elapsedRealtime(); final long sampleDelta = now - state.mSpeedSampleStart; if (sampleDelta > 500) { final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000) / sampleDelta; if (state.mSpeed == 0) { state.mSpeed = sampleSpeed; } else { 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; } if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP && now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); state.mBytesNotified = state.mCurrentBytes; state.mTimeLastNotification = now; } } /** * Write a data buffer to the destination file. * @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, OutputStream out) throws StopRequestException { mStorageManager.verifySpaceBeforeWritingToFile( mInfo.mDestination, state.mFilename, bytesRead); boolean forceVerified = false; while (true) { try { out.write(data, 0, bytesRead); return; } catch (IOException ex) { // 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); forceVerified = true; } else { throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, "Failed to write data: " + ex); } } } } /** * 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) throws StopRequestException { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); if (state.mContentLength == -1) { values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); } mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); final boolean lengthMismatched = (state.mContentLength != -1) && (state.mCurrentBytes != state.mContentLength); if (lengthMismatched) { if (cannotResume(state)) { throw new StopRequestException(STATUS_CANNOT_RESUME, "mismatched content length; unable to resume"); } else { 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) || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType); } /** * Read some data from the HTTP response stream, handling I/O errors. * @param data buffer to use to read data * @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, byte[] data, InputStream entityStream) throws StopRequestException { try { return entityStream.read(data); } catch (IOException ex) { // 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)) { throw new StopRequestException(STATUS_CANNOT_RESUME, "Failed reading response: " + ex + "; unable to resume", ex); } else { throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Failed reading response: " + ex, ex); } } } /** * Prepare target file based on given network response. Derives filename and * target size as needed. */ private void processResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { // TODO: fallocate the entire file if header gave us specific length readResponseHeaders(state, conn); state.mFilename = Helpers.generateSaveFile( mContext, mInfo.mUri, mInfo.mHint, state.mContentDisposition, state.mContentLocation, state.mMimeType, mInfo.mDestination, state.mContentLength, mStorageManager); updateDatabaseFromHeaders(state); // check connectivity again now that we know the total size checkConnectivity(); } /** * Update necessary database fields based on values of HTTP response headers that have been * read. */ private void updateDatabaseFromHeaders(State state) { ContentValues values = new ContentValues(); values.put(Downloads.Impl._DATA, state.mFilename); if (state.mHeaderETag != null) { values.put(Constants.ETAG, state.mHeaderETag); } if (state.mMimeType != null) { values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); } values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes); mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); } /** * Read headers from the HTTP response and store them into local state. */ private void readResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { state.mContentDisposition = conn.getHeaderField("Content-Disposition"); state.mContentLocation = conn.getHeaderField("Content-Location"); if (state.mMimeType == null) { state.mMimeType = Intent.normalizeMimeType(conn.getContentType()); } state.mHeaderETag = conn.getHeaderField("ETag"); if (state.mHeaderETag == null) { state.mHeaderETag = QRD_ETAG; } final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); if (transferEncoding == null) { state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); } else { Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined"); state.mContentLength = -1; } state.mTotalBytes = state.mContentLength; mInfo.mTotalBytes = state.mContentLength; 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 void parseRetryAfterHeaders(State state, HttpURLConnection conn) { state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1); 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; } } /** * Prepare the destination file to receive data. If the file already exists, we'll set up * appropriately for resumption. */ 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 + ", and state.mFilename: " + state.mFilename); } if (!Helpers.isFilenameValid(mContext, state.mFilename, mStorageManager.getDownloadDataDirectory())) { // this should never happen throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, "found invalid internal destination filename"); } // We're resuming a download that got interrupted File f = new File(state.mFilename); if (f.exists()) { if (Constants.LOGV) { Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + ", and state.mFilename: " + state.mFilename); } long fileLength = f.length(); if (fileLength == 0) { // The download hadn't actually started, we can restart from scratch if (Constants.LOGVV) { Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting " + state.mFilename); } f.delete(); state.mFilename = null; if (Constants.LOGV) { Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + ", BUT starting from scratch again: "); } } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) { // This should've been caught upon failure if (Constants.LOGVV) { Log.d(TAG, "setupDestinationFile() unable to resume download, deleting " + state.mFilename); } f.delete(); throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, "Trying to resume a download that can't be resumed"); } else { // All right, we'll be able to resume this download if (Constants.LOGV) { Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + ", and starting with file of length: " + fileLength); } state.mCurrentBytes = (int) fileLength; if (mInfo.mTotalBytes != -1) { state.mContentLength = mInfo.mTotalBytes; } state.mHeaderETag = mInfo.mETag; state.mContinuingDownload = true; if (Constants.LOGV) { Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + ", state.mCurrentBytes: " + state.mCurrentBytes + ", and setting mContinuingDownload to true: "); } } } } } /** * Add custom headers for this download to the HTTP request. */ private void addRequestHeaders(State state, HttpURLConnection conn) { for (Pair header : mInfo.getHeaders()) { 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) { if (!state.mHeaderETag.equals(QRD_ETAG)) { conn.addRequestProperty("If-Match", state.mHeaderETag); } } conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-"); } } /** * Stores information about the completed download, and notifies the initiating application. */ private void notifyDownloadCompleted( State state, int finalStatus, String errorMsg, int numFailed) { notifyThroughDatabase(state, finalStatus, errorMsg, numFailed); if (Downloads.Impl.isStatusCompleted(finalStatus)) { mInfo.sendIntentIfRequested(); } } private void notifyThroughDatabase( State state, int finalStatus, String errorMsg, int numFailed) { ContentValues values = new ContentValues(); 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(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); } mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); } private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() { @Override public void onUidRulesChanged(int uid, int uidRules) { // caller is NPMS, since we only register with them if (uid == mInfo.mUid) { mPolicyDirty = true; } } @Override public void onMeteredIfacesChanged(String[] meteredIfaces) { // caller is NPMS, since we only register with them mPolicyDirty = true; } @Override public void onRestrictBackgroundChanged(boolean restrictBackground) { // caller is NPMS, since we only register with them 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; } } }