Home | History | Annotate | Download | only in downloads
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.providers.downloads;
     18 
     19 import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
     20 import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME;
     21 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
     22 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
     23 import static android.provider.Downloads.Impl.STATUS_SUCCESS;
     24 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
     25 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
     26 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
     27 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
     28 import static com.android.providers.downloads.Constants.TAG;
     29 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
     30 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
     31 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
     32 import static java.net.HttpURLConnection.HTTP_OK;
     33 import static java.net.HttpURLConnection.HTTP_PARTIAL;
     34 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
     35 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
     36 
     37 import android.content.ContentValues;
     38 import android.content.Context;
     39 import android.content.Intent;
     40 import android.drm.DrmManagerClient;
     41 import android.drm.DrmOutputStream;
     42 import android.net.ConnectivityManager;
     43 import android.net.INetworkPolicyListener;
     44 import android.net.NetworkInfo;
     45 import android.net.NetworkPolicyManager;
     46 import android.net.TrafficStats;
     47 import android.os.FileUtils;
     48 import android.os.PowerManager;
     49 import android.os.Process;
     50 import android.os.SystemClock;
     51 import android.os.WorkSource;
     52 import android.provider.Downloads;
     53 import android.text.TextUtils;
     54 import android.util.Log;
     55 import android.util.Pair;
     56 
     57 import com.android.providers.downloads.DownloadInfo.NetworkState;
     58 
     59 import libcore.io.IoUtils;
     60 
     61 import java.io.File;
     62 import java.io.FileDescriptor;
     63 import java.io.FileOutputStream;
     64 import java.io.IOException;
     65 import java.io.InputStream;
     66 import java.io.OutputStream;
     67 import java.io.RandomAccessFile;
     68 import java.net.HttpURLConnection;
     69 import java.net.MalformedURLException;
     70 import java.net.URL;
     71 import java.net.URLConnection;
     72 
     73 /**
     74  * Task which executes a given {@link DownloadInfo}: making network requests,
     75  * persisting data to disk, and updating {@link DownloadProvider}.
     76  */
     77 public class DownloadThread implements Runnable {
     78 
     79     // TODO: bind each download to a specific network interface to avoid state
     80     // checking races once we have ConnectivityManager API
     81 
     82     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
     83     private static final int HTTP_TEMP_REDIRECT = 307;
     84 
     85     private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
     86 
     87     private final Context mContext;
     88     private final DownloadInfo mInfo;
     89     private final SystemFacade mSystemFacade;
     90     private final StorageManager mStorageManager;
     91     private final DownloadNotifier mNotifier;
     92 
     93     private volatile boolean mPolicyDirty;
     94 
     95     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
     96             StorageManager storageManager, DownloadNotifier notifier) {
     97         mContext = context;
     98         mSystemFacade = systemFacade;
     99         mInfo = info;
    100         mStorageManager = storageManager;
    101         mNotifier = notifier;
    102     }
    103 
    104     /**
    105      * Returns the user agent provided by the initiating app, or use the default one
    106      */
    107     private String userAgent() {
    108         String userAgent = mInfo.mUserAgent;
    109         if (userAgent == null) {
    110             userAgent = Constants.DEFAULT_USER_AGENT;
    111         }
    112         return userAgent;
    113     }
    114 
    115     /**
    116      * State for the entire run() method.
    117      */
    118     static class State {
    119         public String mFilename;
    120         public String mMimeType;
    121         public int mRetryAfter = 0;
    122         public boolean mGotData = false;
    123         public String mRequestUri;
    124         public long mTotalBytes = -1;
    125         public long mCurrentBytes = 0;
    126         public String mHeaderETag;
    127         public boolean mContinuingDownload = false;
    128         public long mBytesNotified = 0;
    129         public long mTimeLastNotification = 0;
    130         public int mNetworkType = ConnectivityManager.TYPE_NONE;
    131 
    132         /** Historical bytes/second speed of this download. */
    133         public long mSpeed;
    134         /** Time when current sample started. */
    135         public long mSpeedSampleStart;
    136         /** Bytes transferred since current sample started. */
    137         public long mSpeedSampleBytes;
    138 
    139         public long mContentLength = -1;
    140         public String mContentDisposition;
    141         public String mContentLocation;
    142 
    143         public int mRedirectionCount;
    144         public URL mUrl;
    145 
    146         public State(DownloadInfo info) {
    147             mMimeType = Intent.normalizeMimeType(info.mMimeType);
    148             mRequestUri = info.mUri;
    149             mFilename = info.mFileName;
    150             mTotalBytes = info.mTotalBytes;
    151             mCurrentBytes = info.mCurrentBytes;
    152         }
    153 
    154         public void resetBeforeExecute() {
    155             // Reset any state from previous execution
    156             mContentLength = -1;
    157             mContentDisposition = null;
    158             mContentLocation = null;
    159             mRedirectionCount = 0;
    160         }
    161     }
    162 
    163     @Override
    164     public void run() {
    165         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    166         try {
    167             runInternal();
    168         } finally {
    169             mNotifier.notifyDownloadSpeed(mInfo.mId, 0);
    170         }
    171     }
    172 
    173     private void runInternal() {
    174         // Skip when download already marked as finished; this download was
    175         // probably started again while racing with UpdateThread.
    176         if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
    177                 == Downloads.Impl.STATUS_SUCCESS) {
    178             Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
    179             return;
    180         }
    181 
    182         State state = new State(mInfo);
    183         PowerManager.WakeLock wakeLock = null;
    184         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
    185         int numFailed = mInfo.mNumFailed;
    186         String errorMsg = null;
    187 
    188         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
    189         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
    190 
    191         try {
    192             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
    193             wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
    194             wakeLock.acquire();
    195 
    196             // while performing download, register for rules updates
    197             netPolicy.registerListener(mPolicyListener);
    198 
    199             Log.i(Constants.TAG, "Download " + mInfo.mId + " starting");
    200 
    201             // Remember which network this download started on; used to
    202             // determine if errors were due to network changes.
    203             final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
    204             if (info != null) {
    205                 state.mNetworkType = info.getType();
    206             }
    207 
    208             // Network traffic on this thread should be counted against the
    209             // requesting UID, and is tagged with well-known value.
    210             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
    211             TrafficStats.setThreadStatsUid(mInfo.mUid);
    212 
    213             try {
    214                 // TODO: migrate URL sanity checking into client side of API
    215                 state.mUrl = new URL(state.mRequestUri);
    216             } catch (MalformedURLException e) {
    217                 throw new StopRequestException(STATUS_BAD_REQUEST, e);
    218             }
    219 
    220             executeDownload(state);
    221 
    222             finalizeDestinationFile(state);
    223             finalStatus = Downloads.Impl.STATUS_SUCCESS;
    224         } catch (StopRequestException error) {
    225             // remove the cause before printing, in case it contains PII
    226             errorMsg = error.getMessage();
    227             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
    228             Log.w(Constants.TAG, msg);
    229             if (Constants.LOGV) {
    230                 Log.w(Constants.TAG, msg, error);
    231             }
    232             finalStatus = error.getFinalStatus();
    233 
    234             // Nobody below our level should request retries, since we handle
    235             // failure counts at this level.
    236             if (finalStatus == STATUS_WAITING_TO_RETRY) {
    237                 throw new IllegalStateException("Execution should always throw final error codes");
    238             }
    239 
    240             // Some errors should be retryable, unless we fail too many times.
    241             if (isStatusRetryable(finalStatus)) {
    242                 if (state.mGotData) {
    243                     numFailed = 1;
    244                 } else {
    245                     numFailed += 1;
    246                 }
    247 
    248                 if (numFailed < Constants.MAX_RETRIES) {
    249                     final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
    250                     if (info != null && info.getType() == state.mNetworkType
    251                             && info.isConnected()) {
    252                         // Underlying network is still intact, use normal backoff
    253                         finalStatus = STATUS_WAITING_TO_RETRY;
    254                     } else {
    255                         // Network changed, retry on any next available
    256                         finalStatus = STATUS_WAITING_FOR_NETWORK;
    257                     }
    258                 }
    259             }
    260 
    261             // fall through to finally block
    262         } catch (Throwable ex) {
    263             errorMsg = ex.getMessage();
    264             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
    265             Log.w(Constants.TAG, msg, ex);
    266             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
    267             // falls through to the code that reports an error
    268         } finally {
    269             if (finalStatus == STATUS_SUCCESS) {
    270                 TrafficStats.incrementOperationCount(1);
    271             }
    272 
    273             TrafficStats.clearThreadStatsTag();
    274             TrafficStats.clearThreadStatsUid();
    275 
    276             cleanupDestination(state, finalStatus);
    277             notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
    278 
    279             Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status "
    280                     + Downloads.Impl.statusToString(finalStatus));
    281 
    282             netPolicy.unregisterListener(mPolicyListener);
    283 
    284             if (wakeLock != null) {
    285                 wakeLock.release();
    286                 wakeLock = null;
    287             }
    288         }
    289         mStorageManager.incrementNumDownloadsSoFar();
    290     }
    291 
    292     /**
    293      * Fully execute a single download request. Setup and send the request,
    294      * handle the response, and transfer the data to the destination file.
    295      */
    296     private void executeDownload(State state) throws StopRequestException {
    297         state.resetBeforeExecute();
    298         setupDestinationFile(state);
    299 
    300         // skip when already finished; remove after fixing race in 5217390
    301         if (state.mCurrentBytes == state.mTotalBytes) {
    302             Log.i(Constants.TAG, "Skipping initiating request for download " +
    303                   mInfo.mId + "; already completed");
    304             return;
    305         }
    306 
    307         while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) {
    308             // Open connection and follow any redirects until we have a useful
    309             // response with body.
    310             HttpURLConnection conn = null;
    311             try {
    312                 checkConnectivity();
    313                 conn = (HttpURLConnection) state.mUrl.openConnection();
    314                 conn.setInstanceFollowRedirects(false);
    315                 conn.setConnectTimeout(DEFAULT_TIMEOUT);
    316                 conn.setReadTimeout(DEFAULT_TIMEOUT);
    317 
    318                 addRequestHeaders(state, conn);
    319 
    320                 final int responseCode = conn.getResponseCode();
    321                 switch (responseCode) {
    322                     case HTTP_OK:
    323                         if (state.mContinuingDownload) {
    324                             throw new StopRequestException(
    325                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
    326                         }
    327                         processResponseHeaders(state, conn);
    328                         transferData(state, conn);
    329                         return;
    330 
    331                     case HTTP_PARTIAL:
    332                         if (!state.mContinuingDownload) {
    333                             throw new StopRequestException(
    334                                     STATUS_CANNOT_RESUME, "Expected OK, but received partial");
    335                         }
    336                         transferData(state, conn);
    337                         return;
    338 
    339                     case HTTP_MOVED_PERM:
    340                     case HTTP_MOVED_TEMP:
    341                     case HTTP_SEE_OTHER:
    342                     case HTTP_TEMP_REDIRECT:
    343                         final String location = conn.getHeaderField("Location");
    344                         state.mUrl = new URL(state.mUrl, location);
    345                         if (responseCode == HTTP_MOVED_PERM) {
    346                             // Push updated URL back to database
    347                             state.mRequestUri = state.mUrl.toString();
    348                         }
    349                         continue;
    350 
    351                     case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
    352                         throw new StopRequestException(
    353                                 STATUS_CANNOT_RESUME, "Requested range not satisfiable");
    354 
    355                     case HTTP_UNAVAILABLE:
    356                         parseRetryAfterHeaders(state, conn);
    357                         throw new StopRequestException(
    358                                 HTTP_UNAVAILABLE, conn.getResponseMessage());
    359 
    360                     case HTTP_INTERNAL_ERROR:
    361                         throw new StopRequestException(
    362                                 HTTP_INTERNAL_ERROR, conn.getResponseMessage());
    363 
    364                     default:
    365                         StopRequestException.throwUnhandledHttpError(
    366                                 responseCode, conn.getResponseMessage());
    367                 }
    368             } catch (IOException e) {
    369                 // Trouble with low-level sockets
    370                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
    371 
    372             } finally {
    373                 if (conn != null) conn.disconnect();
    374             }
    375         }
    376 
    377         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
    378     }
    379 
    380     /**
    381      * Transfer data from the given connection to the destination file.
    382      */
    383     private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
    384         DrmManagerClient drmClient = null;
    385         InputStream in = null;
    386         OutputStream out = null;
    387         FileDescriptor outFd = null;
    388         try {
    389             try {
    390                 in = conn.getInputStream();
    391             } catch (IOException e) {
    392                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
    393             }
    394 
    395             try {
    396                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
    397                     drmClient = new DrmManagerClient(mContext);
    398                     final RandomAccessFile file = new RandomAccessFile(
    399                             new File(state.mFilename), "rw");
    400                     out = new DrmOutputStream(drmClient, file, state.mMimeType);
    401                     outFd = file.getFD();
    402                 } else {
    403                     out = new FileOutputStream(state.mFilename, true);
    404                     outFd = ((FileOutputStream) out).getFD();
    405                 }
    406             } catch (IOException e) {
    407                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    408             }
    409 
    410             // Start streaming data, periodically watch for pause/cancel
    411             // commands and checking disk space as needed.
    412             transferData(state, in, out);
    413 
    414             try {
    415                 if (out instanceof DrmOutputStream) {
    416                     ((DrmOutputStream) out).finish();
    417                 }
    418             } catch (IOException e) {
    419                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    420             }
    421 
    422         } finally {
    423             if (drmClient != null) {
    424                 drmClient.release();
    425             }
    426 
    427             IoUtils.closeQuietly(in);
    428 
    429             try {
    430                 if (out != null) out.flush();
    431                 if (outFd != null) outFd.sync();
    432             } catch (IOException e) {
    433             } finally {
    434                 IoUtils.closeQuietly(out);
    435             }
    436         }
    437     }
    438 
    439     /**
    440      * Check if current connectivity is valid for this request.
    441      */
    442     private void checkConnectivity() throws StopRequestException {
    443         // checking connectivity will apply current policy
    444         mPolicyDirty = false;
    445 
    446         final NetworkState networkUsable = mInfo.checkCanUseNetwork();
    447         if (networkUsable != NetworkState.OK) {
    448             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
    449             if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
    450                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
    451                 mInfo.notifyPauseDueToSize(true);
    452             } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
    453                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
    454                 mInfo.notifyPauseDueToSize(false);
    455             }
    456             throw new StopRequestException(status, networkUsable.name());
    457         }
    458     }
    459 
    460     /**
    461      * Transfer as much data as possible from the HTTP response to the
    462      * destination file.
    463      */
    464     private void transferData(State state, InputStream in, OutputStream out)
    465             throws StopRequestException {
    466         final byte data[] = new byte[Constants.BUFFER_SIZE];
    467         for (;;) {
    468             int bytesRead = readFromResponse(state, data, in);
    469             if (bytesRead == -1) { // success, end of stream already reached
    470                 handleEndOfStream(state);
    471                 return;
    472             }
    473 
    474             state.mGotData = true;
    475             writeDataToDestination(state, data, bytesRead, out);
    476             state.mCurrentBytes += bytesRead;
    477             reportProgress(state);
    478 
    479             if (Constants.LOGVV) {
    480                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
    481                       + mInfo.mUri);
    482             }
    483 
    484             checkPausedOrCanceled(state);
    485         }
    486     }
    487 
    488     /**
    489      * Called after a successful completion to take any necessary action on the downloaded file.
    490      */
    491     private void finalizeDestinationFile(State state) {
    492         if (state.mFilename != null) {
    493             // make sure the file is readable
    494             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
    495         }
    496     }
    497 
    498     /**
    499      * Called just before the thread finishes, regardless of status, to take any necessary action on
    500      * the downloaded file.
    501      */
    502     private void cleanupDestination(State state, int finalStatus) {
    503         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
    504             if (Constants.LOGVV) {
    505                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
    506             }
    507             new File(state.mFilename).delete();
    508             state.mFilename = null;
    509         }
    510     }
    511 
    512     /**
    513      * Check if the download has been paused or canceled, stopping the request appropriately if it
    514      * has been.
    515      */
    516     private void checkPausedOrCanceled(State state) throws StopRequestException {
    517         synchronized (mInfo) {
    518             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
    519                 throw new StopRequestException(
    520                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
    521             }
    522             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) {
    523                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
    524             }
    525         }
    526 
    527         // if policy has been changed, trigger connectivity check
    528         if (mPolicyDirty) {
    529             checkConnectivity();
    530         }
    531     }
    532 
    533     /**
    534      * Report download progress through the database if necessary.
    535      */
    536     private void reportProgress(State state) {
    537         final long now = SystemClock.elapsedRealtime();
    538 
    539         final long sampleDelta = now - state.mSpeedSampleStart;
    540         if (sampleDelta > 500) {
    541             final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
    542                     / sampleDelta;
    543 
    544             if (state.mSpeed == 0) {
    545                 state.mSpeed = sampleSpeed;
    546             } else {
    547                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
    548             }
    549 
    550             // Only notify once we have a full sample window
    551             if (state.mSpeedSampleStart != 0) {
    552                 mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed);
    553             }
    554 
    555             state.mSpeedSampleStart = now;
    556             state.mSpeedSampleBytes = state.mCurrentBytes;
    557         }
    558 
    559         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
    560             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
    561             ContentValues values = new ContentValues();
    562             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
    563             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    564             state.mBytesNotified = state.mCurrentBytes;
    565             state.mTimeLastNotification = now;
    566         }
    567     }
    568 
    569     /**
    570      * Write a data buffer to the destination file.
    571      * @param data buffer containing the data to write
    572      * @param bytesRead how many bytes to write from the buffer
    573      */
    574     private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
    575             throws StopRequestException {
    576         mStorageManager.verifySpaceBeforeWritingToFile(
    577                 mInfo.mDestination, state.mFilename, bytesRead);
    578 
    579         boolean forceVerified = false;
    580         while (true) {
    581             try {
    582                 out.write(data, 0, bytesRead);
    583                 return;
    584             } catch (IOException ex) {
    585                 // TODO: better differentiate between DRM and disk failures
    586                 if (!forceVerified) {
    587                     // couldn't write to file. are we out of space? check.
    588                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
    589                     forceVerified = true;
    590                 } else {
    591                     throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
    592                             "Failed to write data: " + ex);
    593                 }
    594             }
    595         }
    596     }
    597 
    598     /**
    599      * Called when we've reached the end of the HTTP response stream, to update the database and
    600      * check for consistency.
    601      */
    602     private void handleEndOfStream(State state) throws StopRequestException {
    603         ContentValues values = new ContentValues();
    604         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
    605         if (state.mContentLength == -1) {
    606             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
    607         }
    608         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    609 
    610         final boolean lengthMismatched = (state.mContentLength != -1)
    611                 && (state.mCurrentBytes != state.mContentLength);
    612         if (lengthMismatched) {
    613             if (cannotResume(state)) {
    614                 throw new StopRequestException(STATUS_CANNOT_RESUME,
    615                         "mismatched content length; unable to resume");
    616             } else {
    617                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
    618                         "closed socket before end of file");
    619             }
    620         }
    621     }
    622 
    623     private boolean cannotResume(State state) {
    624         return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
    625                 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
    626     }
    627 
    628     /**
    629      * Read some data from the HTTP response stream, handling I/O errors.
    630      * @param data buffer to use to read data
    631      * @param entityStream stream for reading the HTTP response entity
    632      * @return the number of bytes actually read or -1 if the end of the stream has been reached
    633      */
    634     private int readFromResponse(State state, byte[] data, InputStream entityStream)
    635             throws StopRequestException {
    636         try {
    637             return entityStream.read(data);
    638         } catch (IOException ex) {
    639             // TODO: handle stream errors the same as other retries
    640             if ("unexpected end of stream".equals(ex.getMessage())) {
    641                 return -1;
    642             }
    643 
    644             ContentValues values = new ContentValues();
    645             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
    646             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    647             if (cannotResume(state)) {
    648                 throw new StopRequestException(STATUS_CANNOT_RESUME,
    649                         "Failed reading response: " + ex + "; unable to resume", ex);
    650             } else {
    651                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
    652                         "Failed reading response: " + ex, ex);
    653             }
    654         }
    655     }
    656 
    657     /**
    658      * Prepare target file based on given network response. Derives filename and
    659      * target size as needed.
    660      */
    661     private void processResponseHeaders(State state, HttpURLConnection conn)
    662             throws StopRequestException {
    663         // TODO: fallocate the entire file if header gave us specific length
    664 
    665         readResponseHeaders(state, conn);
    666 
    667         state.mFilename = Helpers.generateSaveFile(
    668                 mContext,
    669                 mInfo.mUri,
    670                 mInfo.mHint,
    671                 state.mContentDisposition,
    672                 state.mContentLocation,
    673                 state.mMimeType,
    674                 mInfo.mDestination,
    675                 state.mContentLength,
    676                 mStorageManager);
    677 
    678         updateDatabaseFromHeaders(state);
    679         // check connectivity again now that we know the total size
    680         checkConnectivity();
    681     }
    682 
    683     /**
    684      * Update necessary database fields based on values of HTTP response headers that have been
    685      * read.
    686      */
    687     private void updateDatabaseFromHeaders(State state) {
    688         ContentValues values = new ContentValues();
    689         values.put(Downloads.Impl._DATA, state.mFilename);
    690         if (state.mHeaderETag != null) {
    691             values.put(Constants.ETAG, state.mHeaderETag);
    692         }
    693         if (state.mMimeType != null) {
    694             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
    695         }
    696         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
    697         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    698     }
    699 
    700     /**
    701      * Read headers from the HTTP response and store them into local state.
    702      */
    703     private void readResponseHeaders(State state, HttpURLConnection conn)
    704             throws StopRequestException {
    705         state.mContentDisposition = conn.getHeaderField("Content-Disposition");
    706         state.mContentLocation = conn.getHeaderField("Content-Location");
    707 
    708         if (state.mMimeType == null) {
    709             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
    710         }
    711 
    712         state.mHeaderETag = conn.getHeaderField("ETag");
    713 
    714         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
    715         if (transferEncoding == null) {
    716             state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
    717         } else {
    718             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
    719             state.mContentLength = -1;
    720         }
    721 
    722         state.mTotalBytes = state.mContentLength;
    723         mInfo.mTotalBytes = state.mContentLength;
    724 
    725         final boolean noSizeInfo = state.mContentLength == -1
    726                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
    727         if (!mInfo.mNoIntegrity && noSizeInfo) {
    728             throw new StopRequestException(STATUS_CANNOT_RESUME,
    729                     "can't know size of download, giving up");
    730         }
    731     }
    732 
    733     private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
    734         state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
    735         if (state.mRetryAfter < 0) {
    736             state.mRetryAfter = 0;
    737         } else {
    738             if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
    739                 state.mRetryAfter = Constants.MIN_RETRY_AFTER;
    740             } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
    741                 state.mRetryAfter = Constants.MAX_RETRY_AFTER;
    742             }
    743             state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
    744             state.mRetryAfter *= 1000;
    745         }
    746     }
    747 
    748     /**
    749      * Prepare the destination file to receive data.  If the file already exists, we'll set up
    750      * appropriately for resumption.
    751      */
    752     private void setupDestinationFile(State state) throws StopRequestException {
    753         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
    754             if (Constants.LOGV) {
    755                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
    756                         ", and state.mFilename: " + state.mFilename);
    757             }
    758             if (!Helpers.isFilenameValid(state.mFilename,
    759                     mStorageManager.getDownloadDataDirectory())) {
    760                 // this should never happen
    761                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
    762                         "found invalid internal destination filename");
    763             }
    764             // We're resuming a download that got interrupted
    765             File f = new File(state.mFilename);
    766             if (f.exists()) {
    767                 if (Constants.LOGV) {
    768                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    769                             ", and state.mFilename: " + state.mFilename);
    770                 }
    771                 long fileLength = f.length();
    772                 if (fileLength == 0) {
    773                     // The download hadn't actually started, we can restart from scratch
    774                     if (Constants.LOGVV) {
    775                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
    776                                 + state.mFilename);
    777                     }
    778                     f.delete();
    779                     state.mFilename = null;
    780                     if (Constants.LOGV) {
    781                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    782                                 ", BUT starting from scratch again: ");
    783                     }
    784                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
    785                     // This should've been caught upon failure
    786                     if (Constants.LOGVV) {
    787                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
    788                                 + state.mFilename);
    789                     }
    790                     f.delete();
    791                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
    792                             "Trying to resume a download that can't be resumed");
    793                 } else {
    794                     // All right, we'll be able to resume this download
    795                     if (Constants.LOGV) {
    796                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    797                                 ", and starting with file of length: " + fileLength);
    798                     }
    799                     state.mCurrentBytes = (int) fileLength;
    800                     if (mInfo.mTotalBytes != -1) {
    801                         state.mContentLength = mInfo.mTotalBytes;
    802                     }
    803                     state.mHeaderETag = mInfo.mETag;
    804                     state.mContinuingDownload = true;
    805                     if (Constants.LOGV) {
    806                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    807                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
    808                                 ", and setting mContinuingDownload to true: ");
    809                     }
    810                 }
    811             }
    812         }
    813     }
    814 
    815     /**
    816      * Add custom headers for this download to the HTTP request.
    817      */
    818     private void addRequestHeaders(State state, HttpURLConnection conn) {
    819         for (Pair<String, String> header : mInfo.getHeaders()) {
    820             conn.addRequestProperty(header.first, header.second);
    821         }
    822 
    823         // Only splice in user agent when not already defined
    824         if (conn.getRequestProperty("User-Agent") == null) {
    825             conn.addRequestProperty("User-Agent", userAgent());
    826         }
    827 
    828         // Defeat transparent gzip compression, since it doesn't allow us to
    829         // easily resume partial downloads.
    830         conn.setRequestProperty("Accept-Encoding", "identity");
    831 
    832         if (state.mContinuingDownload) {
    833             if (state.mHeaderETag != null) {
    834                 conn.addRequestProperty("If-Match", state.mHeaderETag);
    835             }
    836             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
    837         }
    838     }
    839 
    840     /**
    841      * Stores information about the completed download, and notifies the initiating application.
    842      */
    843     private void notifyDownloadCompleted(
    844             State state, int finalStatus, String errorMsg, int numFailed) {
    845         notifyThroughDatabase(state, finalStatus, errorMsg, numFailed);
    846         if (Downloads.Impl.isStatusCompleted(finalStatus)) {
    847             mInfo.sendIntentIfRequested();
    848         }
    849     }
    850 
    851     private void notifyThroughDatabase(
    852             State state, int finalStatus, String errorMsg, int numFailed) {
    853         ContentValues values = new ContentValues();
    854         values.put(Downloads.Impl.COLUMN_STATUS, finalStatus);
    855         values.put(Downloads.Impl._DATA, state.mFilename);
    856         values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
    857         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
    858         values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed);
    859         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter);
    860 
    861         if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) {
    862             values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri);
    863         }
    864 
    865         // save the error message. could be useful to developers.
    866         if (!TextUtils.isEmpty(errorMsg)) {
    867             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
    868         }
    869         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    870     }
    871 
    872     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
    873         @Override
    874         public void onUidRulesChanged(int uid, int uidRules) {
    875             // caller is NPMS, since we only register with them
    876             if (uid == mInfo.mUid) {
    877                 mPolicyDirty = true;
    878             }
    879         }
    880 
    881         @Override
    882         public void onMeteredIfacesChanged(String[] meteredIfaces) {
    883             // caller is NPMS, since we only register with them
    884             mPolicyDirty = true;
    885         }
    886 
    887         @Override
    888         public void onRestrictBackgroundChanged(boolean restrictBackground) {
    889             // caller is NPMS, since we only register with them
    890             mPolicyDirty = true;
    891         }
    892     };
    893 
    894     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
    895         try {
    896             return Long.parseLong(conn.getHeaderField(field));
    897         } catch (NumberFormatException e) {
    898             return defaultValue;
    899         }
    900     }
    901 
    902     /**
    903      * Return if given status is eligible to be treated as
    904      * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
    905      */
    906     public static boolean isStatusRetryable(int status) {
    907         switch (status) {
    908             case STATUS_HTTP_DATA_ERROR:
    909             case HTTP_UNAVAILABLE:
    910             case HTTP_INTERNAL_ERROR:
    911                 return true;
    912             default:
    913                 return false;
    914         }
    915     }
    916 }
    917