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 com.android.providers.downloads.Constants.TAG;
     20 
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.net.INetworkPolicyListener;
     25 import android.net.NetworkPolicyManager;
     26 import android.net.Proxy;
     27 import android.net.TrafficStats;
     28 import android.net.http.AndroidHttpClient;
     29 import android.os.FileUtils;
     30 import android.os.PowerManager;
     31 import android.os.Process;
     32 import android.provider.Downloads;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import android.util.Pair;
     36 import android.util.Slog;
     37 
     38 import org.apache.http.Header;
     39 import org.apache.http.HttpResponse;
     40 import org.apache.http.client.methods.HttpGet;
     41 import org.apache.http.conn.params.ConnRouteParams;
     42 
     43 import java.io.File;
     44 import java.io.FileNotFoundException;
     45 import java.io.FileOutputStream;
     46 import java.io.IOException;
     47 import java.io.InputStream;
     48 import java.io.SyncFailedException;
     49 import java.net.URI;
     50 import java.net.URISyntaxException;
     51 
     52 /**
     53  * Runs an actual download
     54  */
     55 public class DownloadThread extends Thread {
     56 
     57     private final Context mContext;
     58     private final DownloadInfo mInfo;
     59     private final SystemFacade mSystemFacade;
     60     private final StorageManager mStorageManager;
     61     private DrmConvertSession mDrmConvertSession;
     62 
     63     private volatile boolean mPolicyDirty;
     64 
     65     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
     66             StorageManager storageManager) {
     67         mContext = context;
     68         mSystemFacade = systemFacade;
     69         mInfo = info;
     70         mStorageManager = storageManager;
     71     }
     72 
     73     /**
     74      * Returns the user agent provided by the initiating app, or use the default one
     75      */
     76     private String userAgent() {
     77         String userAgent = mInfo.mUserAgent;
     78         if (userAgent == null) {
     79             userAgent = Constants.DEFAULT_USER_AGENT;
     80         }
     81         return userAgent;
     82     }
     83 
     84     /**
     85      * State for the entire run() method.
     86      */
     87     static class State {
     88         public String mFilename;
     89         public FileOutputStream mStream;
     90         public String mMimeType;
     91         public boolean mCountRetry = false;
     92         public int mRetryAfter = 0;
     93         public int mRedirectCount = 0;
     94         public String mNewUri;
     95         public boolean mGotData = false;
     96         public String mRequestUri;
     97         public long mTotalBytes = -1;
     98         public long mCurrentBytes = 0;
     99         public String mHeaderETag;
    100         public boolean mContinuingDownload = false;
    101         public long mBytesNotified = 0;
    102         public long mTimeLastNotification = 0;
    103 
    104         public State(DownloadInfo info) {
    105             mMimeType = Intent.normalizeMimeType(info.mMimeType);
    106             mRequestUri = info.mUri;
    107             mFilename = info.mFileName;
    108             mTotalBytes = info.mTotalBytes;
    109             mCurrentBytes = info.mCurrentBytes;
    110         }
    111     }
    112 
    113     /**
    114      * State within executeDownload()
    115      */
    116     private static class InnerState {
    117         public String mHeaderContentLength;
    118         public String mHeaderContentDisposition;
    119         public String mHeaderContentLocation;
    120     }
    121 
    122     /**
    123      * Raised from methods called by executeDownload() to indicate that the download should be
    124      * retried immediately.
    125      */
    126     private class RetryDownload extends Throwable {}
    127 
    128     /**
    129      * Executes the download in a separate thread
    130      */
    131     @Override
    132     public void run() {
    133         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    134 
    135         State state = new State(mInfo);
    136         AndroidHttpClient client = null;
    137         PowerManager.WakeLock wakeLock = null;
    138         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
    139         String errorMsg = null;
    140 
    141         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
    142         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
    143 
    144         try {
    145             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
    146             wakeLock.acquire();
    147 
    148             // while performing download, register for rules updates
    149             netPolicy.registerListener(mPolicyListener);
    150 
    151             if (Constants.LOGV) {
    152                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
    153             }
    154 
    155             client = AndroidHttpClient.newInstance(userAgent(), mContext);
    156 
    157             // network traffic on this thread should be counted against the
    158             // requesting uid, and is tagged with well-known value.
    159             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
    160             TrafficStats.setThreadStatsUid(mInfo.mUid);
    161 
    162             boolean finished = false;
    163             while(!finished) {
    164                 Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
    165                 // Set or unset proxy, which may have changed since last GET request.
    166                 // setDefaultProxy() supports null as proxy parameter.
    167                 ConnRouteParams.setDefaultProxy(client.getParams(),
    168                         Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
    169                 HttpGet request = new HttpGet(state.mRequestUri);
    170                 try {
    171                     executeDownload(state, client, request);
    172                     finished = true;
    173                 } catch (RetryDownload exc) {
    174                     // fall through
    175                 } finally {
    176                     request.abort();
    177                     request = null;
    178                 }
    179             }
    180 
    181             if (Constants.LOGV) {
    182                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
    183             }
    184             finalizeDestinationFile(state);
    185             finalStatus = Downloads.Impl.STATUS_SUCCESS;
    186         } catch (StopRequestException error) {
    187             // remove the cause before printing, in case it contains PII
    188             errorMsg = error.getMessage();
    189             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
    190             Log.w(Constants.TAG, msg);
    191             if (Constants.LOGV) {
    192                 Log.w(Constants.TAG, msg, error);
    193             }
    194             finalStatus = error.mFinalStatus;
    195             // fall through to finally block
    196         } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
    197             errorMsg = ex.getMessage();
    198             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
    199             Log.w(Constants.TAG, msg, ex);
    200             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
    201             // falls through to the code that reports an error
    202         } finally {
    203             TrafficStats.clearThreadStatsTag();
    204             TrafficStats.clearThreadStatsUid();
    205 
    206             if (client != null) {
    207                 client.close();
    208                 client = null;
    209             }
    210             cleanupDestination(state, finalStatus);
    211             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
    212                                     state.mGotData, state.mFilename,
    213                                     state.mNewUri, state.mMimeType, errorMsg);
    214             DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
    215 
    216             netPolicy.unregisterListener(mPolicyListener);
    217 
    218             if (wakeLock != null) {
    219                 wakeLock.release();
    220                 wakeLock = null;
    221             }
    222         }
    223         mStorageManager.incrementNumDownloadsSoFar();
    224     }
    225 
    226     /**
    227      * Fully execute a single download request - setup and send the request, handle the response,
    228      * and transfer the data to the destination file.
    229      */
    230     private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
    231             throws StopRequestException, RetryDownload {
    232         InnerState innerState = new InnerState();
    233         byte data[] = new byte[Constants.BUFFER_SIZE];
    234 
    235         setupDestinationFile(state, innerState);
    236         addRequestHeaders(state, request);
    237 
    238         // skip when already finished; remove after fixing race in 5217390
    239         if (state.mCurrentBytes == state.mTotalBytes) {
    240             Log.i(Constants.TAG, "Skipping initiating request for download " +
    241                   mInfo.mId + "; already completed");
    242             return;
    243         }
    244 
    245         // check just before sending the request to avoid using an invalid connection at all
    246         checkConnectivity();
    247 
    248         HttpResponse response = sendRequest(state, client, request);
    249         handleExceptionalStatus(state, innerState, response);
    250 
    251         if (Constants.LOGV) {
    252             Log.v(Constants.TAG, "received response for " + mInfo.mUri);
    253         }
    254 
    255         processResponseHeaders(state, innerState, response);
    256         InputStream entityStream = openResponseEntity(state, response);
    257         transferData(state, innerState, data, entityStream);
    258     }
    259 
    260     /**
    261      * Check if current connectivity is valid for this request.
    262      */
    263     private void checkConnectivity() throws StopRequestException {
    264         // checking connectivity will apply current policy
    265         mPolicyDirty = false;
    266 
    267         int networkUsable = mInfo.checkCanUseNetwork();
    268         if (networkUsable != DownloadInfo.NETWORK_OK) {
    269             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
    270             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
    271                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
    272                 mInfo.notifyPauseDueToSize(true);
    273             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
    274                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
    275                 mInfo.notifyPauseDueToSize(false);
    276             }
    277             throw new StopRequestException(status,
    278                     mInfo.getLogMessageForNetworkError(networkUsable));
    279         }
    280     }
    281 
    282     /**
    283      * Transfer as much data as possible from the HTTP response to the destination file.
    284      * @param data buffer to use to read data
    285      * @param entityStream stream for reading the HTTP response entity
    286      */
    287     private void transferData(
    288             State state, InnerState innerState, byte[] data, InputStream entityStream)
    289             throws StopRequestException {
    290         for (;;) {
    291             int bytesRead = readFromResponse(state, innerState, data, entityStream);
    292             if (bytesRead == -1) { // success, end of stream already reached
    293                 handleEndOfStream(state, innerState);
    294                 return;
    295             }
    296 
    297             state.mGotData = true;
    298             writeDataToDestination(state, data, bytesRead);
    299             state.mCurrentBytes += bytesRead;
    300             reportProgress(state, innerState);
    301 
    302             if (Constants.LOGVV) {
    303                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
    304                       + mInfo.mUri);
    305             }
    306 
    307             checkPausedOrCanceled(state);
    308         }
    309     }
    310 
    311     /**
    312      * Called after a successful completion to take any necessary action on the downloaded file.
    313      */
    314     private void finalizeDestinationFile(State state) throws StopRequestException {
    315         if (state.mFilename != null) {
    316             // make sure the file is readable
    317             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
    318             syncDestination(state);
    319         }
    320     }
    321 
    322     /**
    323      * Called just before the thread finishes, regardless of status, to take any necessary action on
    324      * the downloaded file.
    325      */
    326     private void cleanupDestination(State state, int finalStatus) {
    327         if (mDrmConvertSession != null) {
    328             finalStatus = mDrmConvertSession.close(state.mFilename);
    329         }
    330 
    331         closeDestination(state);
    332         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
    333             Slog.d(TAG, "cleanupDestination() deleting " + state.mFilename);
    334             new File(state.mFilename).delete();
    335             state.mFilename = null;
    336         }
    337     }
    338 
    339     /**
    340      * Sync the destination file to storage.
    341      */
    342     private void syncDestination(State state) {
    343         FileOutputStream downloadedFileStream = null;
    344         try {
    345             downloadedFileStream = new FileOutputStream(state.mFilename, true);
    346             downloadedFileStream.getFD().sync();
    347         } catch (FileNotFoundException ex) {
    348             Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
    349         } catch (SyncFailedException ex) {
    350             Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
    351         } catch (IOException ex) {
    352             Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
    353         } catch (RuntimeException ex) {
    354             Log.w(Constants.TAG, "exception while syncing file: ", ex);
    355         } finally {
    356             if(downloadedFileStream != null) {
    357                 try {
    358                     downloadedFileStream.close();
    359                 } catch (IOException ex) {
    360                     Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
    361                 } catch (RuntimeException ex) {
    362                     Log.w(Constants.TAG, "exception while closing file: ", ex);
    363                 }
    364             }
    365         }
    366     }
    367 
    368     /**
    369      * Close the destination output stream.
    370      */
    371     private void closeDestination(State state) {
    372         try {
    373             // close the file
    374             if (state.mStream != null) {
    375                 state.mStream.close();
    376                 state.mStream = null;
    377             }
    378         } catch (IOException ex) {
    379             if (Constants.LOGV) {
    380                 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
    381             }
    382             // nothing can really be done if the file can't be closed
    383         }
    384     }
    385 
    386     /**
    387      * Check if the download has been paused or canceled, stopping the request appropriately if it
    388      * has been.
    389      */
    390     private void checkPausedOrCanceled(State state) throws StopRequestException {
    391         synchronized (mInfo) {
    392             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
    393                 throw new StopRequestException(
    394                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
    395             }
    396             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
    397                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
    398             }
    399         }
    400 
    401         // if policy has been changed, trigger connectivity check
    402         if (mPolicyDirty) {
    403             checkConnectivity();
    404         }
    405     }
    406 
    407     /**
    408      * Report download progress through the database if necessary.
    409      */
    410     private void reportProgress(State state, InnerState innerState) {
    411         long now = mSystemFacade.currentTimeMillis();
    412         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
    413             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
    414             ContentValues values = new ContentValues();
    415             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
    416             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    417             state.mBytesNotified = state.mCurrentBytes;
    418             state.mTimeLastNotification = now;
    419         }
    420     }
    421 
    422     /**
    423      * Write a data buffer to the destination file.
    424      * @param data buffer containing the data to write
    425      * @param bytesRead how many bytes to write from the buffer
    426      */
    427     private void writeDataToDestination(State state, byte[] data, int bytesRead)
    428             throws StopRequestException {
    429         for (;;) {
    430             try {
    431                 if (state.mStream == null) {
    432                     state.mStream = new FileOutputStream(state.mFilename, true);
    433                 }
    434                 mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
    435                         bytesRead);
    436                 if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) {
    437                     state.mStream.write(data, 0, bytesRead);
    438                 } else {
    439                     byte[] convertedData = mDrmConvertSession.convert(data, bytesRead);
    440                     if (convertedData != null) {
    441                         state.mStream.write(convertedData, 0, convertedData.length);
    442                     } else {
    443                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
    444                                 "Error converting drm data.");
    445                     }
    446                 }
    447                 return;
    448             } catch (IOException ex) {
    449                 // couldn't write to file. are we out of space? check.
    450                 // TODO this check should only be done once. why is this being done
    451                 // in a while(true) loop (see the enclosing statement: for(;;)
    452                 if (state.mStream != null) {
    453                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
    454                 }
    455             } finally {
    456                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
    457                     closeDestination(state);
    458                 }
    459             }
    460         }
    461     }
    462 
    463     /**
    464      * Called when we've reached the end of the HTTP response stream, to update the database and
    465      * check for consistency.
    466      */
    467     private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
    468         ContentValues values = new ContentValues();
    469         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
    470         if (innerState.mHeaderContentLength == null) {
    471             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
    472         }
    473         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    474 
    475         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
    476                 && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength));
    477         if (lengthMismatched) {
    478             if (cannotResume(state)) {
    479                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
    480                         "mismatched content length");
    481             } else {
    482                 throw new StopRequestException(getFinalStatusForHttpError(state),
    483                         "closed socket before end of file");
    484             }
    485         }
    486     }
    487 
    488     private boolean cannotResume(State state) {
    489         return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null;
    490     }
    491 
    492     /**
    493      * Read some data from the HTTP response stream, handling I/O errors.
    494      * @param data buffer to use to read data
    495      * @param entityStream stream for reading the HTTP response entity
    496      * @return the number of bytes actually read or -1 if the end of the stream has been reached
    497      */
    498     private int readFromResponse(State state, InnerState innerState, byte[] data,
    499                                  InputStream entityStream) throws StopRequestException {
    500         try {
    501             return entityStream.read(data);
    502         } catch (IOException ex) {
    503             logNetworkState(mInfo.mUid);
    504             ContentValues values = new ContentValues();
    505             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
    506             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    507             if (cannotResume(state)) {
    508                 String message = "while reading response: " + ex.toString()
    509                 + ", can't resume interrupted download with no ETag";
    510                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
    511                         message, ex);
    512             } else {
    513                 throw new StopRequestException(getFinalStatusForHttpError(state),
    514                         "while reading response: " + ex.toString(), ex);
    515             }
    516         }
    517     }
    518 
    519     /**
    520      * Open a stream for the HTTP response entity, handling I/O errors.
    521      * @return an InputStream to read the response entity
    522      */
    523     private InputStream openResponseEntity(State state, HttpResponse response)
    524             throws StopRequestException {
    525         try {
    526             return response.getEntity().getContent();
    527         } catch (IOException ex) {
    528             logNetworkState(mInfo.mUid);
    529             throw new StopRequestException(getFinalStatusForHttpError(state),
    530                     "while getting entity: " + ex.toString(), ex);
    531         }
    532     }
    533 
    534     private void logNetworkState(int uid) {
    535         if (Constants.LOGX) {
    536             Log.i(Constants.TAG,
    537                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down"));
    538         }
    539     }
    540 
    541     /**
    542      * Read HTTP response headers and take appropriate action, including setting up the destination
    543      * file and updating the database.
    544      */
    545     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
    546             throws StopRequestException {
    547         if (state.mContinuingDownload) {
    548             // ignore response headers on resume requests
    549             return;
    550         }
    551 
    552         readResponseHeaders(state, innerState, response);
    553         if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
    554             mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
    555             if (mDrmConvertSession == null) {
    556                 throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
    557                         + state.mMimeType + " can not be converted.");
    558             }
    559         }
    560 
    561         state.mFilename = Helpers.generateSaveFile(
    562                 mContext,
    563                 mInfo.mUri,
    564                 mInfo.mHint,
    565                 innerState.mHeaderContentDisposition,
    566                 innerState.mHeaderContentLocation,
    567                 state.mMimeType,
    568                 mInfo.mDestination,
    569                 (innerState.mHeaderContentLength != null) ?
    570                         Long.parseLong(innerState.mHeaderContentLength) : 0,
    571                 mInfo.mIsPublicApi, mStorageManager);
    572         try {
    573             state.mStream = new FileOutputStream(state.mFilename);
    574         } catch (FileNotFoundException exc) {
    575             throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
    576                     "while opening destination file: " + exc.toString(), exc);
    577         }
    578         if (Constants.LOGV) {
    579             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
    580         }
    581 
    582         updateDatabaseFromHeaders(state, innerState);
    583         // check connectivity again now that we know the total size
    584         checkConnectivity();
    585     }
    586 
    587     /**
    588      * Update necessary database fields based on values of HTTP response headers that have been
    589      * read.
    590      */
    591     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
    592         ContentValues values = new ContentValues();
    593         values.put(Downloads.Impl._DATA, state.mFilename);
    594         if (state.mHeaderETag != null) {
    595             values.put(Constants.ETAG, state.mHeaderETag);
    596         }
    597         if (state.mMimeType != null) {
    598             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
    599         }
    600         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
    601         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    602     }
    603 
    604     /**
    605      * Read headers from the HTTP response and store them into local state.
    606      */
    607     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
    608             throws StopRequestException {
    609         Header header = response.getFirstHeader("Content-Disposition");
    610         if (header != null) {
    611             innerState.mHeaderContentDisposition = header.getValue();
    612         }
    613         header = response.getFirstHeader("Content-Location");
    614         if (header != null) {
    615             innerState.mHeaderContentLocation = header.getValue();
    616         }
    617         if (state.mMimeType == null) {
    618             header = response.getFirstHeader("Content-Type");
    619             if (header != null) {
    620                 state.mMimeType = Intent.normalizeMimeType(header.getValue());
    621             }
    622         }
    623         header = response.getFirstHeader("ETag");
    624         if (header != null) {
    625             state.mHeaderETag = header.getValue();
    626         }
    627         String headerTransferEncoding = null;
    628         header = response.getFirstHeader("Transfer-Encoding");
    629         if (header != null) {
    630             headerTransferEncoding = header.getValue();
    631         }
    632         if (headerTransferEncoding == null) {
    633             header = response.getFirstHeader("Content-Length");
    634             if (header != null) {
    635                 innerState.mHeaderContentLength = header.getValue();
    636                 state.mTotalBytes = mInfo.mTotalBytes =
    637                         Long.parseLong(innerState.mHeaderContentLength);
    638             }
    639         } else {
    640             // Ignore content-length with transfer-encoding - 2616 4.4 3
    641             if (Constants.LOGVV) {
    642                 Log.v(Constants.TAG,
    643                         "ignoring content-length because of xfer-encoding");
    644             }
    645         }
    646         if (Constants.LOGVV) {
    647             Log.v(Constants.TAG, "Content-Disposition: " +
    648                     innerState.mHeaderContentDisposition);
    649             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
    650             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
    651             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
    652             Log.v(Constants.TAG, "ETag: " + state.mHeaderETag);
    653             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
    654         }
    655 
    656         boolean noSizeInfo = innerState.mHeaderContentLength == null
    657                 && (headerTransferEncoding == null
    658                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
    659         if (!mInfo.mNoIntegrity && noSizeInfo) {
    660             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
    661                     "can't know size of download, giving up");
    662         }
    663     }
    664 
    665     /**
    666      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
    667      */
    668     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
    669             throws StopRequestException, RetryDownload {
    670         int statusCode = response.getStatusLine().getStatusCode();
    671         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
    672             handleServiceUnavailable(state, response);
    673         }
    674         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
    675             handleRedirect(state, response, statusCode);
    676         }
    677 
    678         if (Constants.LOGV) {
    679             Log.i(Constants.TAG, "recevd_status = " + statusCode +
    680                     ", mContinuingDownload = " + state.mContinuingDownload);
    681         }
    682         int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
    683         if (statusCode != expectedStatus) {
    684             handleOtherStatus(state, innerState, statusCode);
    685         }
    686     }
    687 
    688     /**
    689      * Handle a status that we don't know how to deal with properly.
    690      */
    691     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
    692             throws StopRequestException {
    693         if (statusCode == 416) {
    694             // range request failed. it should never fail.
    695             throw new IllegalStateException("Http Range request failure: totalBytes = " +
    696                     state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
    697         }
    698         int finalStatus;
    699         if (Downloads.Impl.isStatusError(statusCode)) {
    700             finalStatus = statusCode;
    701         } else if (statusCode >= 300 && statusCode < 400) {
    702             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
    703         } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
    704             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
    705         } else {
    706             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
    707         }
    708         throw new StopRequestException(finalStatus, "http error " +
    709                 statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
    710     }
    711 
    712     /**
    713      * Handle a 3xx redirect status.
    714      */
    715     private void handleRedirect(State state, HttpResponse response, int statusCode)
    716             throws StopRequestException, RetryDownload {
    717         if (Constants.LOGVV) {
    718             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
    719         }
    720         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
    721             throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
    722                     "too many redirects");
    723         }
    724         Header header = response.getFirstHeader("Location");
    725         if (header == null) {
    726             return;
    727         }
    728         if (Constants.LOGVV) {
    729             Log.v(Constants.TAG, "Location :" + header.getValue());
    730         }
    731 
    732         String newUri;
    733         try {
    734             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
    735         } catch(URISyntaxException ex) {
    736             if (Constants.LOGV) {
    737                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
    738                         + " for " + mInfo.mUri);
    739             }
    740             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
    741                     "Couldn't resolve redirect URI");
    742         }
    743         ++state.mRedirectCount;
    744         state.mRequestUri = newUri;
    745         if (statusCode == 301 || statusCode == 303) {
    746             // use the new URI for all future requests (should a retry/resume be necessary)
    747             state.mNewUri = newUri;
    748         }
    749         throw new RetryDownload();
    750     }
    751 
    752     /**
    753      * Handle a 503 Service Unavailable status by processing the Retry-After header.
    754      */
    755     private void handleServiceUnavailable(State state, HttpResponse response)
    756             throws StopRequestException {
    757         if (Constants.LOGVV) {
    758             Log.v(Constants.TAG, "got HTTP response code 503");
    759         }
    760         state.mCountRetry = true;
    761         Header header = response.getFirstHeader("Retry-After");
    762         if (header != null) {
    763            try {
    764                if (Constants.LOGVV) {
    765                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
    766                }
    767                state.mRetryAfter = Integer.parseInt(header.getValue());
    768                if (state.mRetryAfter < 0) {
    769                    state.mRetryAfter = 0;
    770                } else {
    771                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
    772                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
    773                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
    774                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
    775                    }
    776                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
    777                    state.mRetryAfter *= 1000;
    778                }
    779            } catch (NumberFormatException ex) {
    780                // ignored - retryAfter stays 0 in this case.
    781            }
    782         }
    783         throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
    784                 "got 503 Service Unavailable, will retry later");
    785     }
    786 
    787     /**
    788      * Send the request to the server, handling any I/O exceptions.
    789      */
    790     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
    791             throws StopRequestException {
    792         try {
    793             return client.execute(request);
    794         } catch (IllegalArgumentException ex) {
    795             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
    796                     "while trying to execute request: " + ex.toString(), ex);
    797         } catch (IOException ex) {
    798             logNetworkState(mInfo.mUid);
    799             throw new StopRequestException(getFinalStatusForHttpError(state),
    800                     "while trying to execute request: " + ex.toString(), ex);
    801         }
    802     }
    803 
    804     private int getFinalStatusForHttpError(State state) {
    805         int networkUsable = mInfo.checkCanUseNetwork();
    806         if (networkUsable != DownloadInfo.NETWORK_OK) {
    807             switch (networkUsable) {
    808                 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
    809                 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
    810                     return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
    811                 default:
    812                     return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
    813             }
    814         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
    815             state.mCountRetry = true;
    816             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
    817         } else {
    818             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
    819             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
    820         }
    821     }
    822 
    823     /**
    824      * Prepare the destination file to receive data.  If the file already exists, we'll set up
    825      * appropriately for resumption.
    826      */
    827     private void setupDestinationFile(State state, InnerState innerState)
    828             throws StopRequestException {
    829         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
    830             if (Constants.LOGV) {
    831                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
    832                         ", and state.mFilename: " + state.mFilename);
    833             }
    834             if (!Helpers.isFilenameValid(state.mFilename,
    835                     mStorageManager.getDownloadDataDirectory())) {
    836                 // this should never happen
    837                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
    838                         "found invalid internal destination filename");
    839             }
    840             // We're resuming a download that got interrupted
    841             File f = new File(state.mFilename);
    842             if (f.exists()) {
    843                 if (Constants.LOGV) {
    844                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    845                             ", and state.mFilename: " + state.mFilename);
    846                 }
    847                 long fileLength = f.length();
    848                 if (fileLength == 0) {
    849                     // The download hadn't actually started, we can restart from scratch
    850                     Slog.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
    851                             + state.mFilename);
    852                     f.delete();
    853                     state.mFilename = null;
    854                     if (Constants.LOGV) {
    855                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    856                                 ", BUT starting from scratch again: ");
    857                     }
    858                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
    859                     // This should've been caught upon failure
    860                     Slog.d(TAG, "setupDestinationFile() unable to resume download, deleting "
    861                             + state.mFilename);
    862                     f.delete();
    863                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
    864                             "Trying to resume a download that can't be resumed");
    865                 } else {
    866                     // All right, we'll be able to resume this download
    867                     if (Constants.LOGV) {
    868                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    869                                 ", and starting with file of length: " + fileLength);
    870                     }
    871                     try {
    872                         state.mStream = new FileOutputStream(state.mFilename, true);
    873                     } catch (FileNotFoundException exc) {
    874                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
    875                                 "while opening destination for resuming: " + exc.toString(), exc);
    876                     }
    877                     state.mCurrentBytes = (int) fileLength;
    878                     if (mInfo.mTotalBytes != -1) {
    879                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
    880                     }
    881                     state.mHeaderETag = mInfo.mETag;
    882                     state.mContinuingDownload = true;
    883                     if (Constants.LOGV) {
    884                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
    885                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
    886                                 ", and setting mContinuingDownload to true: ");
    887                     }
    888                 }
    889             }
    890         }
    891 
    892         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
    893             closeDestination(state);
    894         }
    895     }
    896 
    897     /**
    898      * Add custom headers for this download to the HTTP request.
    899      */
    900     private void addRequestHeaders(State state, HttpGet request) {
    901         for (Pair<String, String> header : mInfo.getHeaders()) {
    902             request.addHeader(header.first, header.second);
    903         }
    904 
    905         if (state.mContinuingDownload) {
    906             if (state.mHeaderETag != null) {
    907                 request.addHeader("If-Match", state.mHeaderETag);
    908             }
    909             request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
    910             if (Constants.LOGV) {
    911                 Log.i(Constants.TAG, "Adding Range header: " +
    912                         "bytes=" + state.mCurrentBytes + "-");
    913                 Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
    914             }
    915         }
    916     }
    917 
    918     /**
    919      * Stores information about the completed download, and notifies the initiating application.
    920      */
    921     private void notifyDownloadCompleted(
    922             int status, boolean countRetry, int retryAfter, boolean gotData,
    923             String filename, String uri, String mimeType, String errorMsg) {
    924         notifyThroughDatabase(
    925                 status, countRetry, retryAfter, gotData, filename, uri, mimeType,
    926                 errorMsg);
    927         if (Downloads.Impl.isStatusCompleted(status)) {
    928             mInfo.sendIntentIfRequested();
    929         }
    930     }
    931 
    932     private void notifyThroughDatabase(
    933             int status, boolean countRetry, int retryAfter, boolean gotData,
    934             String filename, String uri, String mimeType, String errorMsg) {
    935         ContentValues values = new ContentValues();
    936         values.put(Downloads.Impl.COLUMN_STATUS, status);
    937         values.put(Downloads.Impl._DATA, filename);
    938         if (uri != null) {
    939             values.put(Downloads.Impl.COLUMN_URI, uri);
    940         }
    941         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
    942         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
    943         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
    944         if (!countRetry) {
    945             values.put(Constants.FAILED_CONNECTIONS, 0);
    946         } else if (gotData) {
    947             values.put(Constants.FAILED_CONNECTIONS, 1);
    948         } else {
    949             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
    950         }
    951         // save the error message. could be useful to developers.
    952         if (!TextUtils.isEmpty(errorMsg)) {
    953             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
    954         }
    955         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    956     }
    957 
    958     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
    959         @Override
    960         public void onUidRulesChanged(int uid, int uidRules) {
    961             // caller is NPMS, since we only register with them
    962             if (uid == mInfo.mUid) {
    963                 mPolicyDirty = true;
    964             }
    965         }
    966 
    967         @Override
    968         public void onMeteredIfacesChanged(String[] meteredIfaces) {
    969             // caller is NPMS, since we only register with them
    970             mPolicyDirty = true;
    971         }
    972 
    973         @Override
    974         public void onRestrictBackgroundChanged(boolean restrictBackground) {
    975             // caller is NPMS, since we only register with them
    976             mPolicyDirty = true;
    977         }
    978     };
    979 }
    980