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