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.COLUMN_CONTROL;
     20 import static android.provider.Downloads.Impl.COLUMN_DELETED;
     21 import static android.provider.Downloads.Impl.COLUMN_STATUS;
     22 import static android.provider.Downloads.Impl.CONTROL_PAUSED;
     23 import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
     24 import static android.provider.Downloads.Impl.STATUS_CANCELED;
     25 import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME;
     26 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
     27 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
     28 import static android.provider.Downloads.Impl.STATUS_PAUSED_BY_APP;
     29 import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
     30 import static android.provider.Downloads.Impl.STATUS_RUNNING;
     31 import static android.provider.Downloads.Impl.STATUS_SUCCESS;
     32 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
     33 import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
     34 import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR;
     35 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
     36 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
     37 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
     38 
     39 import static com.android.providers.downloads.Constants.TAG;
     40 
     41 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
     42 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
     43 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
     44 import static java.net.HttpURLConnection.HTTP_OK;
     45 import static java.net.HttpURLConnection.HTTP_PARTIAL;
     46 import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
     47 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
     48 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
     49 
     50 import android.app.job.JobInfo;
     51 import android.app.job.JobParameters;
     52 import android.content.ContentValues;
     53 import android.content.Context;
     54 import android.content.Intent;
     55 import android.drm.DrmManagerClient;
     56 import android.drm.DrmOutputStream;
     57 import android.net.ConnectivityManager;
     58 import android.net.INetworkPolicyListener;
     59 import android.net.Network;
     60 import android.net.NetworkInfo;
     61 import android.net.NetworkPolicyManager;
     62 import android.net.TrafficStats;
     63 import android.net.Uri;
     64 import android.os.ParcelFileDescriptor;
     65 import android.os.Process;
     66 import android.os.SystemClock;
     67 import android.provider.Downloads;
     68 import android.system.ErrnoException;
     69 import android.system.Os;
     70 import android.system.OsConstants;
     71 import android.util.Log;
     72 import android.util.MathUtils;
     73 import android.util.Pair;
     74 
     75 import libcore.io.IoUtils;
     76 
     77 import java.io.File;
     78 import java.io.FileDescriptor;
     79 import java.io.FileNotFoundException;
     80 import java.io.IOException;
     81 import java.io.InputStream;
     82 import java.io.OutputStream;
     83 import java.net.HttpURLConnection;
     84 import java.net.MalformedURLException;
     85 import java.net.ProtocolException;
     86 import java.net.URL;
     87 import java.net.URLConnection;
     88 import java.security.GeneralSecurityException;
     89 
     90 import javax.net.ssl.HttpsURLConnection;
     91 import javax.net.ssl.SSLContext;
     92 
     93 /**
     94  * Task which executes a given {@link DownloadInfo}: making network requests,
     95  * persisting data to disk, and updating {@link DownloadProvider}.
     96  * <p>
     97  * To know if a download is successful, we need to know either the final content
     98  * length to expect, or the transfer to be chunked. To resume an interrupted
     99  * download, we need an ETag.
    100  * <p>
    101  * Failed network requests are retried several times before giving up. Local
    102  * disk errors fail immediately and are not retried.
    103  */
    104 public class DownloadThread extends Thread {
    105 
    106     // TODO: bind each download to a specific network interface to avoid state
    107     // checking races once we have ConnectivityManager API
    108 
    109     // TODO: add support for saving to content://
    110 
    111     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    112     private static final int HTTP_TEMP_REDIRECT = 307;
    113 
    114     private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
    115 
    116     private final Context mContext;
    117     private final SystemFacade mSystemFacade;
    118     private final DownloadNotifier mNotifier;
    119     private final NetworkPolicyManager mNetworkPolicy;
    120 
    121     private final DownloadJobService mJobService;
    122     private final JobParameters mParams;
    123 
    124     private final long mId;
    125 
    126     /**
    127      * Info object that should be treated as read-only. Any potentially mutated
    128      * fields are tracked in {@link #mInfoDelta}. If a field exists in
    129      * {@link #mInfoDelta}, it must not be read from {@link #mInfo}.
    130      */
    131     private final DownloadInfo mInfo;
    132     private final DownloadInfoDelta mInfoDelta;
    133 
    134     private volatile boolean mPolicyDirty;
    135 
    136     /**
    137      * Local changes to {@link DownloadInfo}. These are kept local to avoid
    138      * racing with the thread that updates based on change notifications.
    139      */
    140     private class DownloadInfoDelta {
    141         public String mUri;
    142         public String mFileName;
    143         public String mMimeType;
    144         public int mStatus;
    145         public int mNumFailed;
    146         public int mRetryAfter;
    147         public long mTotalBytes;
    148         public long mCurrentBytes;
    149         public String mETag;
    150 
    151         public String mErrorMsg;
    152 
    153         private static final String NOT_CANCELED = COLUMN_STATUS + " != '" + STATUS_CANCELED + "'";
    154         private static final String NOT_DELETED = COLUMN_DELETED + " == '0'";
    155         private static final String NOT_PAUSED = "(" + COLUMN_CONTROL + " IS NULL OR "
    156                 + COLUMN_CONTROL + " != '" + CONTROL_PAUSED + "')";
    157 
    158         private static final String SELECTION_VALID = NOT_CANCELED + " AND " + NOT_DELETED + " AND "
    159                 + NOT_PAUSED;
    160 
    161         public DownloadInfoDelta(DownloadInfo info) {
    162             mUri = info.mUri;
    163             mFileName = info.mFileName;
    164             mMimeType = info.mMimeType;
    165             mStatus = info.mStatus;
    166             mNumFailed = info.mNumFailed;
    167             mRetryAfter = info.mRetryAfter;
    168             mTotalBytes = info.mTotalBytes;
    169             mCurrentBytes = info.mCurrentBytes;
    170             mETag = info.mETag;
    171         }
    172 
    173         private ContentValues buildContentValues() {
    174             final ContentValues values = new ContentValues();
    175 
    176             values.put(Downloads.Impl.COLUMN_URI, mUri);
    177             values.put(Downloads.Impl._DATA, mFileName);
    178             values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
    179             values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
    180             values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
    181             values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
    182             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
    183             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
    184             values.put(Constants.ETAG, mETag);
    185 
    186             values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
    187             values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);
    188 
    189             return values;
    190         }
    191 
    192         /**
    193          * Blindly push update of current delta values to provider.
    194          */
    195         public void writeToDatabase() {
    196             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(),
    197                     null, null);
    198         }
    199 
    200         /**
    201          * Push update of current delta values to provider, asserting strongly
    202          * that we haven't been paused or deleted.
    203          */
    204         public void writeToDatabaseOrThrow() throws StopRequestException {
    205             if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
    206                     buildContentValues(), SELECTION_VALID, null) == 0) {
    207                 if (mInfo.queryDownloadControl() == CONTROL_PAUSED) {
    208                     throw new StopRequestException(STATUS_PAUSED_BY_APP, "Download paused!");
    209                 } else {
    210                     throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
    211                 }
    212             }
    213         }
    214     }
    215 
    216     /**
    217      * Flag indicating if we've made forward progress transferring file data
    218      * from a remote server.
    219      */
    220     private boolean mMadeProgress = false;
    221 
    222     /**
    223      * Details from the last time we pushed a database update.
    224      */
    225     private long mLastUpdateBytes = 0;
    226     private long mLastUpdateTime = 0;
    227 
    228     private boolean mIgnoreBlocked;
    229     private Network mNetwork;
    230 
    231     private int mNetworkType = ConnectivityManager.TYPE_NONE;
    232 
    233     /** Historical bytes/second speed of this download. */
    234     private long mSpeed;
    235     /** Time when current sample started. */
    236     private long mSpeedSampleStart;
    237     /** Bytes transferred since current sample started. */
    238     private long mSpeedSampleBytes;
    239 
    240     /** Flag indicating that thread must be halted */
    241     private volatile boolean mShutdownRequested;
    242 
    243     public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) {
    244         mContext = service;
    245         mSystemFacade = Helpers.getSystemFacade(mContext);
    246         mNotifier = Helpers.getDownloadNotifier(mContext);
    247         mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class);
    248 
    249         mJobService = service;
    250         mParams = params;
    251 
    252         mId = info.mId;
    253         mInfo = info;
    254         mInfoDelta = new DownloadInfoDelta(info);
    255     }
    256 
    257     @Override
    258     public void run() {
    259         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    260 
    261         // Skip when download already marked as finished; this download was
    262         // probably started again while racing with UpdateThread.
    263         if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) {
    264             logDebug("Already finished; skipping");
    265             return;
    266         }
    267 
    268         try {
    269             // while performing download, register for rules updates
    270             mNetworkPolicy.registerListener(mPolicyListener);
    271 
    272             logDebug("Starting");
    273 
    274             mInfoDelta.mStatus = STATUS_RUNNING;
    275             mInfoDelta.writeToDatabase();
    276 
    277             // If we're showing a foreground notification for the requesting
    278             // app, the download isn't affected by the blocked status of the
    279             // requesting app
    280             mIgnoreBlocked = mInfo.isVisible();
    281 
    282             // Use the caller's default network to make this connection, since
    283             // they might be subject to restrictions that we shouldn't let them
    284             // circumvent
    285             mNetwork = mSystemFacade.getActiveNetwork(mInfo.mUid, mIgnoreBlocked);
    286             if (mNetwork == null) {
    287                 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK,
    288                         "No network associated with requesting UID");
    289             }
    290 
    291             // Remember which network this download started on; used to
    292             // determine if errors were due to network changes.
    293             final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
    294                     mIgnoreBlocked);
    295             if (info != null) {
    296                 mNetworkType = info.getType();
    297             }
    298 
    299             // Network traffic on this thread should be counted against the
    300             // requesting UID, and is tagged with well-known value.
    301             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
    302             TrafficStats.setThreadStatsUid(mInfo.mUid);
    303 
    304             executeDownload();
    305 
    306             mInfoDelta.mStatus = STATUS_SUCCESS;
    307             TrafficStats.incrementOperationCount(1);
    308 
    309             // If we just finished a chunked file, record total size
    310             if (mInfoDelta.mTotalBytes == -1) {
    311                 mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes;
    312             }
    313 
    314         } catch (StopRequestException e) {
    315             mInfoDelta.mStatus = e.getFinalStatus();
    316             mInfoDelta.mErrorMsg = e.getMessage();
    317 
    318             logWarning("Stop requested with status "
    319                     + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
    320                     + mInfoDelta.mErrorMsg);
    321 
    322             // Nobody below our level should request retries, since we handle
    323             // failure counts at this level.
    324             if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
    325                 throw new IllegalStateException("Execution should always throw final error codes");
    326             }
    327 
    328             // Some errors should be retryable, unless we fail too many times.
    329             if (isStatusRetryable(mInfoDelta.mStatus)) {
    330                 if (mMadeProgress) {
    331                     mInfoDelta.mNumFailed = 1;
    332                 } else {
    333                     mInfoDelta.mNumFailed += 1;
    334                 }
    335 
    336                 if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
    337                     final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
    338                             mIgnoreBlocked);
    339                     if (info != null && info.getType() == mNetworkType && info.isConnected()) {
    340                         // Underlying network is still intact, use normal backoff
    341                         mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
    342                     } else {
    343                         // Network changed, retry on any next available
    344                         mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
    345                     }
    346 
    347                     if ((mInfoDelta.mETag == null && mMadeProgress)
    348                             || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
    349                         // However, if we wrote data and have no ETag to verify
    350                         // contents against later, we can't actually resume.
    351                         mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
    352                     }
    353                 }
    354             }
    355 
    356             // If we're waiting for a network that must be unmetered, our status
    357             // is actually queued so we show relevant notifications
    358             if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
    359                     && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
    360                 mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI;
    361             }
    362 
    363         } catch (Throwable t) {
    364             mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
    365             mInfoDelta.mErrorMsg = t.toString();
    366 
    367             logError("Failed: " + mInfoDelta.mErrorMsg, t);
    368 
    369         } finally {
    370             logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus));
    371 
    372             mNotifier.notifyDownloadSpeed(mId, 0);
    373 
    374             finalizeDestination();
    375 
    376             mInfoDelta.writeToDatabase();
    377 
    378             TrafficStats.clearThreadStatsTag();
    379             TrafficStats.clearThreadStatsUid();
    380 
    381             mNetworkPolicy.unregisterListener(mPolicyListener);
    382         }
    383 
    384         if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
    385             mInfo.sendIntentIfRequested();
    386             if (mInfo.shouldScanFile(mInfoDelta.mStatus)) {
    387                 DownloadScanner.requestScanBlocking(mContext, mInfo.mId, mInfoDelta.mFileName,
    388                         mInfoDelta.mMimeType);
    389             }
    390         } else if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY
    391                 || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
    392                 || mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) {
    393             Helpers.scheduleJob(mContext, DownloadInfo.queryDownloadInfo(mContext, mId));
    394         }
    395 
    396         mJobService.jobFinishedInternal(mParams, false);
    397     }
    398 
    399     public void requestShutdown() {
    400         mShutdownRequested = true;
    401     }
    402 
    403     /**
    404      * Fully execute a single download request. Setup and send the request,
    405      * handle the response, and transfer the data to the destination file.
    406      */
    407     private void executeDownload() throws StopRequestException {
    408         final boolean resuming = mInfoDelta.mCurrentBytes != 0;
    409 
    410         URL url;
    411         try {
    412             // TODO: migrate URL sanity checking into client side of API
    413             url = new URL(mInfoDelta.mUri);
    414         } catch (MalformedURLException e) {
    415             throw new StopRequestException(STATUS_BAD_REQUEST, e);
    416         }
    417 
    418         boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid);
    419         SSLContext appContext;
    420         try {
    421             appContext = mSystemFacade.getSSLContextForPackage(mContext, mInfo.mPackage);
    422         } catch (GeneralSecurityException e) {
    423             // This should never happen.
    424             throw new StopRequestException(STATUS_UNKNOWN_ERROR, "Unable to create SSLContext.");
    425         }
    426         int redirectionCount = 0;
    427         while (redirectionCount++ < Constants.MAX_REDIRECTS) {
    428             // Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier
    429             // because of HTTP redirects which can change the protocol between HTTP and HTTPS.
    430             if ((!cleartextTrafficPermitted) && ("http".equalsIgnoreCase(url.getProtocol()))) {
    431                 throw new StopRequestException(STATUS_BAD_REQUEST,
    432                         "Cleartext traffic not permitted for UID " + mInfo.mUid + ": "
    433                         + Uri.parse(url.toString()).toSafeString());
    434             }
    435 
    436             // Open connection and follow any redirects until we have a useful
    437             // response with body.
    438             HttpURLConnection conn = null;
    439             try {
    440                 // Check that the caller is allowed to make network connections. If so, make one on
    441                 // their behalf to open the url.
    442                 checkConnectivity();
    443                 conn = (HttpURLConnection) mNetwork.openConnection(url);
    444                 conn.setInstanceFollowRedirects(false);
    445                 conn.setConnectTimeout(DEFAULT_TIMEOUT);
    446                 conn.setReadTimeout(DEFAULT_TIMEOUT);
    447                 // If this is going over HTTPS configure the trust to be the same as the calling
    448                 // package.
    449                 if (conn instanceof HttpsURLConnection) {
    450                     ((HttpsURLConnection)conn).setSSLSocketFactory(appContext.getSocketFactory());
    451                 }
    452 
    453                 addRequestHeaders(conn, resuming);
    454 
    455                 final int responseCode = conn.getResponseCode();
    456                 switch (responseCode) {
    457                     case HTTP_OK:
    458                         if (resuming) {
    459                             throw new StopRequestException(
    460                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
    461                         }
    462                         parseOkHeaders(conn);
    463                         transferData(conn);
    464                         return;
    465 
    466                     case HTTP_PARTIAL:
    467                         if (!resuming) {
    468                             throw new StopRequestException(
    469                                     STATUS_CANNOT_RESUME, "Expected OK, but received partial");
    470                         }
    471                         transferData(conn);
    472                         return;
    473 
    474                     case HTTP_MOVED_PERM:
    475                     case HTTP_MOVED_TEMP:
    476                     case HTTP_SEE_OTHER:
    477                     case HTTP_TEMP_REDIRECT:
    478                         final String location = conn.getHeaderField("Location");
    479                         url = new URL(url, location);
    480                         if (responseCode == HTTP_MOVED_PERM) {
    481                             // Push updated URL back to database
    482                             mInfoDelta.mUri = url.toString();
    483                         }
    484                         continue;
    485 
    486                     case HTTP_PRECON_FAILED:
    487                         throw new StopRequestException(
    488                                 STATUS_CANNOT_RESUME, "Precondition failed");
    489 
    490                     case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
    491                         throw new StopRequestException(
    492                                 STATUS_CANNOT_RESUME, "Requested range not satisfiable");
    493 
    494                     case HTTP_UNAVAILABLE:
    495                         parseUnavailableHeaders(conn);
    496                         throw new StopRequestException(
    497                                 HTTP_UNAVAILABLE, conn.getResponseMessage());
    498 
    499                     case HTTP_INTERNAL_ERROR:
    500                         throw new StopRequestException(
    501                                 HTTP_INTERNAL_ERROR, conn.getResponseMessage());
    502 
    503                     default:
    504                         StopRequestException.throwUnhandledHttpError(
    505                                 responseCode, conn.getResponseMessage());
    506                 }
    507 
    508             } catch (IOException e) {
    509                 if (e instanceof ProtocolException
    510                         && e.getMessage().startsWith("Unexpected status line")) {
    511                     throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e);
    512                 } else {
    513                     // Trouble with low-level sockets
    514                     throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
    515                 }
    516 
    517             } finally {
    518                 if (conn != null) conn.disconnect();
    519             }
    520         }
    521 
    522         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
    523     }
    524 
    525     /**
    526      * Transfer data from the given connection to the destination file.
    527      */
    528     private void transferData(HttpURLConnection conn) throws StopRequestException {
    529 
    530         // To detect when we're really finished, we either need a length, closed
    531         // connection, or chunked encoding.
    532         final boolean hasLength = mInfoDelta.mTotalBytes != -1;
    533         final boolean isConnectionClose = "close".equalsIgnoreCase(
    534                 conn.getHeaderField("Connection"));
    535         final boolean isEncodingChunked = "chunked".equalsIgnoreCase(
    536                 conn.getHeaderField("Transfer-Encoding"));
    537 
    538         final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked;
    539         if (!finishKnown) {
    540             throw new StopRequestException(
    541                     STATUS_CANNOT_RESUME, "can't know size of download, giving up");
    542         }
    543 
    544         DrmManagerClient drmClient = null;
    545         ParcelFileDescriptor outPfd = null;
    546         FileDescriptor outFd = null;
    547         InputStream in = null;
    548         OutputStream out = null;
    549         try {
    550             try {
    551                 in = conn.getInputStream();
    552             } catch (IOException e) {
    553                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
    554             }
    555 
    556             try {
    557                 outPfd = mContext.getContentResolver()
    558                         .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
    559                 outFd = outPfd.getFileDescriptor();
    560 
    561                 if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
    562                     drmClient = new DrmManagerClient(mContext);
    563                     out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);
    564                 } else {
    565                     out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
    566                 }
    567 
    568                 // Pre-flight disk space requirements, when known
    569                 if (mInfoDelta.mTotalBytes > 0) {
    570                     final long curSize = Os.fstat(outFd).st_size;
    571                     final long newBytes = mInfoDelta.mTotalBytes - curSize;
    572 
    573                     StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
    574 
    575                     try {
    576                         // We found enough space, so claim it for ourselves
    577                         Os.posix_fallocate(outFd, 0, mInfoDelta.mTotalBytes);
    578                     } catch (ErrnoException e) {
    579                         if (e.errno == OsConstants.ENOSYS || e.errno == OsConstants.ENOTSUP) {
    580                             Log.w(TAG, "fallocate() not supported; falling back to ftruncate()");
    581                             Os.ftruncate(outFd, mInfoDelta.mTotalBytes);
    582                         } else {
    583                             throw e;
    584                         }
    585                     }
    586                 }
    587 
    588                 // Move into place to begin writing
    589                 Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET);
    590 
    591             } catch (ErrnoException e) {
    592                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    593             } catch (IOException e) {
    594                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    595             }
    596 
    597             // Start streaming data, periodically watch for pause/cancel
    598             // commands and checking disk space as needed.
    599             transferData(in, out, outFd);
    600 
    601             try {
    602                 if (out instanceof DrmOutputStream) {
    603                     ((DrmOutputStream) out).finish();
    604                 }
    605             } catch (IOException e) {
    606                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    607             }
    608 
    609         } finally {
    610             if (drmClient != null) {
    611                 drmClient.close();
    612             }
    613 
    614             IoUtils.closeQuietly(in);
    615 
    616             try {
    617                 if (out != null) out.flush();
    618                 if (outFd != null) outFd.sync();
    619             } catch (IOException e) {
    620             } finally {
    621                 IoUtils.closeQuietly(out);
    622             }
    623         }
    624     }
    625 
    626     /**
    627      * Transfer as much data as possible from the HTTP response to the
    628      * destination file.
    629      */
    630     private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
    631             throws StopRequestException {
    632         final byte buffer[] = new byte[Constants.BUFFER_SIZE];
    633         while (true) {
    634             if (mPolicyDirty) checkConnectivity();
    635 
    636             if (mShutdownRequested) {
    637                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
    638                         "Local halt requested; job probably timed out");
    639             }
    640 
    641             int len = -1;
    642             try {
    643                 len = in.read(buffer);
    644             } catch (IOException e) {
    645                 throw new StopRequestException(
    646                         STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
    647             }
    648 
    649             if (len == -1) {
    650                 break;
    651             }
    652 
    653             try {
    654                 // When streaming, ensure space before each write
    655                 if (mInfoDelta.mTotalBytes == -1) {
    656                     final long curSize = Os.fstat(outFd).st_size;
    657                     final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize;
    658 
    659                     StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
    660                 }
    661 
    662                 out.write(buffer, 0, len);
    663 
    664                 mMadeProgress = true;
    665                 mInfoDelta.mCurrentBytes += len;
    666 
    667                 updateProgress(outFd);
    668 
    669             } catch (ErrnoException e) {
    670                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    671             } catch (IOException e) {
    672                 throw new StopRequestException(STATUS_FILE_ERROR, e);
    673             }
    674         }
    675 
    676         // Finished without error; verify length if known
    677         if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) {
    678             throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch");
    679         }
    680     }
    681 
    682     /**
    683      * Called just before the thread finishes, regardless of status, to take any
    684      * necessary action on the downloaded file.
    685      */
    686     private void finalizeDestination() {
    687         if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) {
    688             // When error, free up any disk space
    689             try {
    690                 final ParcelFileDescriptor target = mContext.getContentResolver()
    691                         .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
    692                 try {
    693                     Os.ftruncate(target.getFileDescriptor(), 0);
    694                 } catch (ErrnoException ignored) {
    695                 } finally {
    696                     IoUtils.closeQuietly(target);
    697                 }
    698             } catch (FileNotFoundException ignored) {
    699             }
    700 
    701             // Delete if local file
    702             if (mInfoDelta.mFileName != null) {
    703                 new File(mInfoDelta.mFileName).delete();
    704                 mInfoDelta.mFileName = null;
    705             }
    706 
    707         } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
    708             // When success, open access if local file
    709             if (mInfoDelta.mFileName != null) {
    710                 if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
    711                     try {
    712                         // Move into final resting place, if needed
    713                         final File before = new File(mInfoDelta.mFileName);
    714                         final File beforeDir = Helpers.getRunningDestinationDirectory(
    715                                 mContext, mInfo.mDestination);
    716                         final File afterDir = Helpers.getSuccessDestinationDirectory(
    717                                 mContext, mInfo.mDestination);
    718                         if (!beforeDir.equals(afterDir)
    719                                 && before.getParentFile().equals(beforeDir)) {
    720                             final File after = new File(afterDir, before.getName());
    721                             if (before.renameTo(after)) {
    722                                 mInfoDelta.mFileName = after.getAbsolutePath();
    723                             }
    724                         }
    725                     } catch (IOException ignored) {
    726                     }
    727                 }
    728             }
    729         }
    730     }
    731 
    732     /**
    733      * Check if current connectivity is valid for this request.
    734      */
    735     private void checkConnectivity() throws StopRequestException {
    736         // checking connectivity will apply current policy
    737         mPolicyDirty = false;
    738 
    739         final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
    740                 mIgnoreBlocked);
    741         if (info == null || !info.isConnected()) {
    742             throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected");
    743         }
    744         if (info.isRoaming() && !mInfo.isRoamingAllowed()) {
    745             throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming");
    746         }
    747         if (info.isMetered() && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
    748             throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is metered");
    749         }
    750     }
    751 
    752     /**
    753      * Report download progress through the database if necessary.
    754      */
    755     private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException {
    756         final long now = SystemClock.elapsedRealtime();
    757         final long currentBytes = mInfoDelta.mCurrentBytes;
    758 
    759         final long sampleDelta = now - mSpeedSampleStart;
    760         if (sampleDelta > 500) {
    761             final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000)
    762                     / sampleDelta;
    763 
    764             if (mSpeed == 0) {
    765                 mSpeed = sampleSpeed;
    766             } else {
    767                 mSpeed = ((mSpeed * 3) + sampleSpeed) / 4;
    768             }
    769 
    770             // Only notify once we have a full sample window
    771             if (mSpeedSampleStart != 0) {
    772                 mNotifier.notifyDownloadSpeed(mId, mSpeed);
    773             }
    774 
    775             mSpeedSampleStart = now;
    776             mSpeedSampleBytes = currentBytes;
    777         }
    778 
    779         final long bytesDelta = currentBytes - mLastUpdateBytes;
    780         final long timeDelta = now - mLastUpdateTime;
    781         if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
    782             // fsync() to ensure that current progress has been flushed to disk,
    783             // so we can always resume based on latest database information.
    784             outFd.sync();
    785 
    786             mInfoDelta.writeToDatabaseOrThrow();
    787 
    788             mLastUpdateBytes = currentBytes;
    789             mLastUpdateTime = now;
    790         }
    791     }
    792 
    793     /**
    794      * Process response headers from first server response. This derives its
    795      * filename, size, and ETag.
    796      */
    797     private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException {
    798         if (mInfoDelta.mFileName == null) {
    799             final String contentDisposition = conn.getHeaderField("Content-Disposition");
    800             final String contentLocation = conn.getHeaderField("Content-Location");
    801 
    802             try {
    803                 mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri,
    804                         mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType,
    805                         mInfo.mDestination);
    806             } catch (IOException e) {
    807                 throw new StopRequestException(
    808                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e);
    809             }
    810         }
    811 
    812         if (mInfoDelta.mMimeType == null) {
    813             mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
    814         }
    815 
    816         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
    817         if (transferEncoding == null) {
    818             mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1);
    819         } else {
    820             mInfoDelta.mTotalBytes = -1;
    821         }
    822 
    823         mInfoDelta.mETag = conn.getHeaderField("ETag");
    824 
    825         mInfoDelta.writeToDatabaseOrThrow();
    826 
    827         // Check connectivity again now that we know the total size
    828         checkConnectivity();
    829     }
    830 
    831     private void parseUnavailableHeaders(HttpURLConnection conn) {
    832         long retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
    833         retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER,
    834                 Constants.MAX_RETRY_AFTER);
    835         mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS);
    836     }
    837 
    838     /**
    839      * Add custom headers for this download to the HTTP request.
    840      */
    841     private void addRequestHeaders(HttpURLConnection conn, boolean resuming) {
    842         for (Pair<String, String> header : mInfo.getHeaders()) {
    843             conn.addRequestProperty(header.first, header.second);
    844         }
    845 
    846         // Only splice in user agent when not already defined
    847         if (conn.getRequestProperty("User-Agent") == null) {
    848             conn.addRequestProperty("User-Agent", mInfo.getUserAgent());
    849         }
    850 
    851         // Defeat transparent gzip compression, since it doesn't allow us to
    852         // easily resume partial downloads.
    853         conn.setRequestProperty("Accept-Encoding", "identity");
    854 
    855         // Defeat connection reuse, since otherwise servers may continue
    856         // streaming large downloads after cancelled.
    857         conn.setRequestProperty("Connection", "close");
    858 
    859         if (resuming) {
    860             if (mInfoDelta.mETag != null) {
    861                 conn.addRequestProperty("If-Match", mInfoDelta.mETag);
    862             }
    863             conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-");
    864         }
    865     }
    866 
    867     private void logDebug(String msg) {
    868         Log.d(TAG, "[" + mId + "] " + msg);
    869     }
    870 
    871     private void logWarning(String msg) {
    872         Log.w(TAG, "[" + mId + "] " + msg);
    873     }
    874 
    875     private void logError(String msg, Throwable t) {
    876         Log.e(TAG, "[" + mId + "] " + msg, t);
    877     }
    878 
    879     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
    880         @Override
    881         public void onUidRulesChanged(int uid, int uidRules) {
    882             // caller is NPMS, since we only register with them
    883             if (uid == mInfo.mUid) {
    884                 mPolicyDirty = true;
    885             }
    886         }
    887 
    888         @Override
    889         public void onMeteredIfacesChanged(String[] meteredIfaces) {
    890             // caller is NPMS, since we only register with them
    891             mPolicyDirty = true;
    892         }
    893 
    894         @Override
    895         public void onRestrictBackgroundChanged(boolean restrictBackground) {
    896             // caller is NPMS, since we only register with them
    897             mPolicyDirty = true;
    898         }
    899 
    900         @Override
    901         public void onRestrictBackgroundWhitelistChanged(int uid, boolean whitelisted) {
    902             // caller is NPMS, since we only register with them
    903             if (uid == mInfo.mUid) {
    904                 mPolicyDirty = true;
    905             }
    906         }
    907 
    908         @Override
    909         public void onRestrictBackgroundBlacklistChanged(int uid, boolean blacklisted) {
    910             // caller is NPMS, since we only register with them
    911             if (uid == mInfo.mUid) {
    912                 mPolicyDirty = true;
    913             }
    914         }
    915     };
    916 
    917     private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
    918         try {
    919             return Long.parseLong(conn.getHeaderField(field));
    920         } catch (NumberFormatException e) {
    921             return defaultValue;
    922         }
    923     }
    924 
    925     /**
    926      * Return if given status is eligible to be treated as
    927      * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
    928      */
    929     public static boolean isStatusRetryable(int status) {
    930         switch (status) {
    931             case STATUS_HTTP_DATA_ERROR:
    932             case HTTP_UNAVAILABLE:
    933             case HTTP_INTERNAL_ERROR:
    934             case STATUS_FILE_ERROR:
    935                 return true;
    936             default:
    937                 return false;
    938         }
    939     }
    940 }
    941