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