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