| /* |
| * 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.text.format.DateUtils.MINUTE_IN_MILLIS; |
| import static com.android.providers.downloads.Constants.TAG; |
| import static java.net.HttpURLConnection.HTTP_OK; |
| import static java.net.HttpURLConnection.HTTP_PARTIAL; |
| 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.INetworkPolicyListener; |
| 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.provider.Downloads; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| 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.URL; |
| import java.net.URLConnection; |
| |
| import libcore.io.IoUtils; |
| |
| /** |
| * Thread which executes a given {@link DownloadInfo}: making network requests, |
| * persisting data to disk, and updating {@link DownloadProvider}. |
| */ |
| public class DownloadThread extends Thread { |
| |
| private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; |
| |
| private static final int DEFAULT_TIMEOUT = (int) MINUTE_IN_MILLIS; |
| |
| private final Context mContext; |
| private final DownloadInfo mInfo; |
| private final SystemFacade mSystemFacade; |
| private final StorageManager mStorageManager; |
| |
| private volatile boolean mPolicyDirty; |
| |
| public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, |
| StorageManager storageManager) { |
| mContext = context; |
| mSystemFacade = systemFacade; |
| mInfo = info; |
| mStorageManager = storageManager; |
| } |
| |
| /** |
| * 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 boolean mCountRetry = false; |
| 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; |
| |
| /** 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 State(DownloadInfo info) { |
| mMimeType = Intent.normalizeMimeType(info.mMimeType); |
| mRequestUri = info.mUri; |
| mFilename = info.mFileName; |
| mTotalBytes = info.mTotalBytes; |
| mCurrentBytes = info.mCurrentBytes; |
| } |
| } |
| |
| /** |
| * State within executeDownload() |
| */ |
| private static class InnerState { |
| public long mContentLength; |
| public String mContentDisposition; |
| public String mContentLocation; |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| |
| 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; |
| 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.acquire(); |
| |
| // while performing download, register for rules updates |
| netPolicy.registerListener(mPolicyListener); |
| |
| if (Constants.LOGV) { |
| Log.v(Constants.TAG, "initiating download for " + mInfo.mUri); |
| } |
| |
| // 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); |
| |
| final URL url = new URL(state.mRequestUri); |
| final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); |
| conn.setConnectTimeout(DEFAULT_TIMEOUT); |
| conn.setReadTimeout(DEFAULT_TIMEOUT); |
| try { |
| executeDownload(state, conn); |
| finished = true; |
| } finally { |
| conn.disconnect(); |
| } |
| } |
| |
| if (Constants.LOGV) { |
| Log.v(Constants.TAG, "download completed for " + mInfo.mUri); |
| } |
| 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.mFinalStatus; |
| // 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 { |
| TrafficStats.clearThreadStatsTag(); |
| TrafficStats.clearThreadStatsUid(); |
| |
| cleanupDestination(state, finalStatus); |
| notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, |
| state.mGotData, state.mFilename, state.mMimeType, errorMsg); |
| |
| 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, HttpURLConnection conn) throws StopRequestException { |
| final InnerState innerState = new InnerState(); |
| |
| setupDestinationFile(state, innerState); |
| addRequestHeaders(state, conn); |
| |
| // 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; |
| } |
| |
| // check just before sending the request to avoid using an invalid connection at all |
| checkConnectivity(); |
| |
| DrmManagerClient drmClient = null; |
| InputStream in = null; |
| OutputStream out = null; |
| FileDescriptor outFd = null; |
| try { |
| try { |
| // Asking for response code will execute the request |
| final int statusCode = conn.getResponseCode(); |
| in = conn.getInputStream(); |
| |
| handleExceptionalStatus(state, innerState, conn, statusCode); |
| processResponseHeaders(state, innerState, conn); |
| } catch (IOException e) { |
| throw new StopRequestException( |
| getFinalStatusForHttpError(state), "Request failed: " + e, 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( |
| Downloads.Impl.STATUS_FILE_ERROR, "Failed to open destination: " + e, e); |
| } |
| |
| transferData(state, innerState, in, out); |
| |
| try { |
| if (out instanceof DrmOutputStream) { |
| ((DrmOutputStream) out).finish(); |
| } |
| } catch (IOException e) { |
| throw new StopRequestException( |
| Downloads.Impl.STATUS_FILE_ERROR, "Failed to finish: " + e, 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; |
| |
| int networkUsable = mInfo.checkCanUseNetwork(); |
| if (networkUsable != DownloadInfo.NETWORK_OK) { |
| int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; |
| if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) { |
| status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; |
| mInfo.notifyPauseDueToSize(true); |
| } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { |
| status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; |
| mInfo.notifyPauseDueToSize(false); |
| } |
| throw new StopRequestException(status, |
| mInfo.getLogMessageForNetworkError(networkUsable)); |
| } |
| } |
| |
| /** |
| * Transfer as much data as possible from the HTTP response to the |
| * destination file. |
| */ |
| private void transferData(State state, InnerState innerState, InputStream in, OutputStream out) |
| throws StopRequestException { |
| final byte data[] = new byte[Constants.BUFFER_SIZE]; |
| for (;;) { |
| int bytesRead = readFromResponse(state, innerState, data, in); |
| if (bytesRead == -1) { // success, end of stream already reached |
| handleEndOfStream(state, innerState); |
| return; |
| } |
| |
| state.mGotData = true; |
| writeDataToDestination(state, data, bytesRead, out); |
| state.mCurrentBytes += bytesRead; |
| reportProgress(state, innerState); |
| |
| 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) { |
| throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); |
| } |
| } |
| |
| // if policy has been changed, trigger connectivity check |
| if (mPolicyDirty) { |
| checkConnectivity(); |
| } |
| } |
| |
| /** |
| * Report download progress through the database if necessary. |
| */ |
| private void reportProgress(State state, InnerState innerState) { |
| 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; |
| } |
| |
| state.mSpeedSampleStart = now; |
| state.mSpeedSampleBytes = state.mCurrentBytes; |
| |
| DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed); |
| } |
| |
| 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, InnerState innerState) throws StopRequestException { |
| ContentValues values = new ContentValues(); |
| values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); |
| if (innerState.mContentLength == -1) { |
| values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); |
| } |
| mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); |
| |
| boolean lengthMismatched = (innerState.mContentLength != -1) |
| && (state.mCurrentBytes != innerState.mContentLength); |
| if (lengthMismatched) { |
| if (cannotResume(state)) { |
| throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, |
| "mismatched content length; unable to resume"); |
| } else { |
| throw new StopRequestException(getFinalStatusForHttpError(state), |
| "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, InnerState innerState, 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(Downloads.Impl.STATUS_CANNOT_RESUME, |
| "Failed reading response: " + ex + "; unable to resume", ex); |
| } else { |
| throw new StopRequestException(getFinalStatusForHttpError(state), |
| "Failed reading response: " + ex, ex); |
| } |
| } |
| } |
| |
| /** |
| * 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, HttpURLConnection conn) |
| throws StopRequestException { |
| if (state.mContinuingDownload) { |
| // ignore response headers on resume requests |
| return; |
| } |
| |
| readResponseHeaders(state, innerState, conn); |
| |
| state.mFilename = Helpers.generateSaveFile( |
| mContext, |
| mInfo.mUri, |
| mInfo.mHint, |
| innerState.mContentDisposition, |
| innerState.mContentLocation, |
| state.mMimeType, |
| mInfo.mDestination, |
| innerState.mContentLength, |
| mInfo.mIsPublicApi, mStorageManager); |
| |
| updateDatabaseFromHeaders(state, innerState); |
| // 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, InnerState innerState) { |
| 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, InnerState innerState, HttpURLConnection conn) |
| throws StopRequestException { |
| innerState.mContentDisposition = conn.getHeaderField("Content-Disposition"); |
| innerState.mContentLocation = conn.getHeaderField("Content-Location"); |
| |
| if (state.mMimeType == null) { |
| state.mMimeType = Intent.normalizeMimeType(conn.getContentType()); |
| } |
| |
| state.mHeaderETag = conn.getHeaderField("ETag"); |
| |
| final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); |
| if (transferEncoding == null) { |
| innerState.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); |
| } else { |
| Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined"); |
| innerState.mContentLength = -1; |
| } |
| |
| state.mTotalBytes = innerState.mContentLength; |
| mInfo.mTotalBytes = innerState.mContentLength; |
| |
| final boolean noSizeInfo = innerState.mContentLength == -1 |
| && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")); |
| if (!mInfo.mNoIntegrity && noSizeInfo) { |
| throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, |
| "can't know size of download, giving up"); |
| } |
| } |
| |
| /** |
| * Check the HTTP response status and handle anything unusual (e.g. not 200/206). |
| */ |
| private void handleExceptionalStatus( |
| State state, InnerState innerState, HttpURLConnection conn, int statusCode) |
| throws StopRequestException { |
| if (statusCode == HTTP_UNAVAILABLE && mInfo.mNumFailed < Constants.MAX_RETRIES) { |
| handleServiceUnavailable(state, conn); |
| } |
| |
| if (Constants.LOGV) { |
| Log.i(Constants.TAG, "recevd_status = " + statusCode + |
| ", mContinuingDownload = " + state.mContinuingDownload); |
| } |
| int expectedStatus = state.mContinuingDownload ? HTTP_PARTIAL : HTTP_OK; |
| if (statusCode != expectedStatus) { |
| handleOtherStatus(state, innerState, statusCode); |
| } |
| } |
| |
| /** |
| * 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 == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) { |
| // 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 (statusCode >= 400 && statusCode < 600) { |
| finalStatus = statusCode; |
| } else if (statusCode >= 300 && statusCode < 400) { |
| finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT; |
| } else if (state.mContinuingDownload && statusCode == HTTP_OK) { |
| finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME; |
| } else { |
| finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; |
| } |
| throw new StopRequestException(finalStatus, "http error " + |
| statusCode + ", mContinuingDownload: " + state.mContinuingDownload); |
| } |
| |
| /** |
| * Handle a 503 Service Unavailable status by processing the Retry-After header. |
| */ |
| private void handleServiceUnavailable(State state, HttpURLConnection conn) |
| throws StopRequestException { |
| state.mCountRetry = true; |
| 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; |
| } |
| |
| throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY, |
| "got 503 Service Unavailable, will retry later"); |
| } |
| |
| 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; |
| } else { |
| Log.w(Constants.TAG, "reached max retries for " + mInfo.mId); |
| return Downloads.Impl.STATUS_HTTP_DATA_ERROR; |
| } |
| } |
| |
| /** |
| * 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 { |
| 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(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) { |
| innerState.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) { |
| conn.addRequestProperty("User-Agent", userAgent()); |
| |
| for (Pair<String, String> header : mInfo.getHeaders()) { |
| conn.addRequestProperty(header.first, header.second); |
| } |
| |
| if (state.mContinuingDownload) { |
| if (state.mHeaderETag != null) { |
| conn.addRequestProperty("If-Match", state.mHeaderETag); |
| } |
| conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-"); |
| if (Constants.LOGV) { |
| Log.i(Constants.TAG, "Adding Range header: " + |
| "bytes=" + state.mCurrentBytes + "-"); |
| Log.i(Constants.TAG, " totalBytes = " + state.mTotalBytes); |
| } |
| } |
| } |
| |
| /** |
| * 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 mimeType, String errorMsg) { |
| notifyThroughDatabase( |
| status, countRetry, retryAfter, gotData, filename, mimeType, errorMsg); |
| if (Downloads.Impl.isStatusCompleted(status)) { |
| mInfo.sendIntentIfRequested(); |
| } |
| } |
| |
| private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter, |
| boolean gotData, String filename, String mimeType, String errorMsg) { |
| ContentValues values = new ContentValues(); |
| values.put(Downloads.Impl.COLUMN_STATUS, status); |
| values.put(Downloads.Impl._DATA, filename); |
| values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType); |
| 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); |
| } |
| // 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; |
| } |
| } |
| } |