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