Cleaner I/O.
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadThread.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20 import static com.android.providers.downloads.Constants.TAG;
21 import static java.net.HttpURLConnection.HTTP_OK;
22 import static java.net.HttpURLConnection.HTTP_PARTIAL;
23 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
24
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.drm.DrmManagerClient;
29 import android.drm.DrmOutputStream;
30 import android.net.INetworkPolicyListener;
31 import android.net.NetworkPolicyManager;
32 import android.net.TrafficStats;
33 import android.os.FileUtils;
34 import android.os.PowerManager;
35 import android.os.Process;
36 import android.os.SystemClock;
37 import android.provider.Downloads;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import android.util.Pair;
41
42 import java.io.File;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.io.SyncFailedException;
49 import java.net.HttpURLConnection;
50 import java.net.URL;
51 import java.net.URLConnection;
52
53 import libcore.io.IoUtils;
54
55 /**
56  * Thread which executes a given {@link DownloadInfo}: making network requests,
57  * persisting data to disk, and updating {@link DownloadProvider}.
58  */
59 public class DownloadThread extends Thread {
60
61     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
62
63     private static final int DEFAULT_TIMEOUT = (int) MINUTE_IN_MILLIS;
64
65     private final Context mContext;
66     private final DownloadInfo mInfo;
67     private final SystemFacade mSystemFacade;
68     private final StorageManager mStorageManager;
69
70     private volatile boolean mPolicyDirty;
71
72     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
73             StorageManager storageManager) {
74         mContext = context;
75         mSystemFacade = systemFacade;
76         mInfo = info;
77         mStorageManager = storageManager;
78     }
79
80     /**
81      * Returns the user agent provided by the initiating app, or use the default one
82      */
83     private String userAgent() {
84         String userAgent = mInfo.mUserAgent;
85         if (userAgent == null) {
86             userAgent = Constants.DEFAULT_USER_AGENT;
87         }
88         return userAgent;
89     }
90
91     /**
92      * State for the entire run() method.
93      */
94     static class State {
95         public String mFilename;
96         public String mMimeType;
97         public boolean mCountRetry = false;
98         public int mRetryAfter = 0;
99         public boolean mGotData = false;
100         public String mRequestUri;
101         public long mTotalBytes = -1;
102         public long mCurrentBytes = 0;
103         public String mHeaderETag;
104         public boolean mContinuingDownload = false;
105         public long mBytesNotified = 0;
106         public long mTimeLastNotification = 0;
107
108         /** Historical bytes/second speed of this download. */
109         public long mSpeed;
110         /** Time when current sample started. */
111         public long mSpeedSampleStart;
112         /** Bytes transferred since current sample started. */
113         public long mSpeedSampleBytes;
114
115         public State(DownloadInfo info) {
116             mMimeType = Intent.normalizeMimeType(info.mMimeType);
117             mRequestUri = info.mUri;
118             mFilename = info.mFileName;
119             mTotalBytes = info.mTotalBytes;
120             mCurrentBytes = info.mCurrentBytes;
121         }
122     }
123
124     /**
125      * State within executeDownload()
126      */
127     private static class InnerState {
128         public long mContentLength;
129         public String mContentDisposition;
130         public String mContentLocation;
131     }
132
133     /**
134      * Executes the download in a separate thread
135      */
136     @Override
137     public void run() {
138         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
139         try {
140             runInternal();
141         } finally {
142             DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
143         }
144     }
145
146     private void runInternal() {
147         // Skip when download already marked as finished; this download was
148         // probably started again while racing with UpdateThread.
149         if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
150                 == Downloads.Impl.STATUS_SUCCESS) {
151             Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
152             return;
153         }
154
155         State state = new State(mInfo);
156         PowerManager.WakeLock wakeLock = null;
157         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
158         String errorMsg = null;
159
160         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
161         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
162
163         try {
164             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
165             wakeLock.acquire();
166
167             // while performing download, register for rules updates
168             netPolicy.registerListener(mPolicyListener);
169
170             if (Constants.LOGV) {
171                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
172             }
173
174             // network traffic on this thread should be counted against the
175             // requesting uid, and is tagged with well-known value.
176             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
177             TrafficStats.setThreadStatsUid(mInfo.mUid);
178
179             boolean finished = false;
180             while (!finished) {
181                 Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
182
183                 final URL url = new URL(state.mRequestUri);
184                 final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
185                 conn.setConnectTimeout(DEFAULT_TIMEOUT);
186                 conn.setReadTimeout(DEFAULT_TIMEOUT);
187                 try {
188                     executeDownload(state, conn);
189                     finished = true;
190                 } finally {
191                     conn.disconnect();
192                 }
193             }
194
195             if (Constants.LOGV) {
196                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
197             }
198             finalizeDestinationFile(state);
199             finalStatus = Downloads.Impl.STATUS_SUCCESS;
200         } catch (StopRequestException error) {
201             // remove the cause before printing, in case it contains PII
202             errorMsg = error.getMessage();
203             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
204             Log.w(Constants.TAG, msg);
205             if (Constants.LOGV) {
206                 Log.w(Constants.TAG, msg, error);
207             }
208             finalStatus = error.mFinalStatus;
209             // fall through to finally block
210         } catch (Throwable ex) {
211             errorMsg = ex.getMessage();
212             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
213             Log.w(Constants.TAG, msg, ex);
214             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
215             // falls through to the code that reports an error
216         } finally {
217             TrafficStats.clearThreadStatsTag();
218             TrafficStats.clearThreadStatsUid();
219
220             cleanupDestination(state, finalStatus);
221             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
222                     state.mGotData, state.mFilename, state.mMimeType, errorMsg);
223
224             netPolicy.unregisterListener(mPolicyListener);
225
226             if (wakeLock != null) {
227                 wakeLock.release();
228                 wakeLock = null;
229             }
230         }
231         mStorageManager.incrementNumDownloadsSoFar();
232     }
233
234     /**
235      * Fully execute a single download request - setup and send the request, handle the response,
236      * and transfer the data to the destination file.
237      */
238     private void executeDownload(State state, HttpURLConnection conn) throws StopRequestException {
239         final InnerState innerState = new InnerState();
240
241         setupDestinationFile(state, innerState);
242         addRequestHeaders(state, conn);
243
244         // skip when already finished; remove after fixing race in 5217390
245         if (state.mCurrentBytes == state.mTotalBytes) {
246             Log.i(Constants.TAG, "Skipping initiating request for download " +
247                   mInfo.mId + "; already completed");
248             return;
249         }
250
251         // check just before sending the request to avoid using an invalid connection at all
252         checkConnectivity();
253
254         InputStream in = null;
255         OutputStream out = null;
256         DrmManagerClient drmClient = null;
257         try {
258             try {
259                 // Asking for response code will execute the request
260                 final int statusCode = conn.getResponseCode();
261                 in = conn.getInputStream();
262
263                 handleExceptionalStatus(state, innerState, conn, statusCode);
264                 processResponseHeaders(state, innerState, conn);
265             } catch (IOException e) {
266                 throw new StopRequestException(
267                         Downloads.Impl.STATUS_HTTP_DATA_ERROR, "Request failed: " + e, e);
268             }
269
270             try {
271                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
272                     drmClient = new DrmManagerClient(mContext);
273                     out = new DrmOutputStream(
274                             drmClient, new File(state.mFilename), state.mMimeType);
275                 } else {
276                     out = new FileOutputStream(state.mFilename);
277                 }
278             } catch (IOException e) {
279                 throw new StopRequestException(
280                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to open destination: " + e, e);
281             }
282
283             transferData(state, innerState, in, out);
284
285         } finally {
286             if (drmClient != null) {
287                 drmClient.release();
288             }
289             IoUtils.closeQuietly(in);
290             IoUtils.closeQuietly(out);
291         }
292     }
293
294     /**
295      * Check if current connectivity is valid for this request.
296      */
297     private void checkConnectivity() throws StopRequestException {
298         // checking connectivity will apply current policy
299         mPolicyDirty = false;
300
301         int networkUsable = mInfo.checkCanUseNetwork();
302         if (networkUsable != DownloadInfo.NETWORK_OK) {
303             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
304             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
305                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
306                 mInfo.notifyPauseDueToSize(true);
307             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
308                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
309                 mInfo.notifyPauseDueToSize(false);
310             }
311             throw new StopRequestException(status,
312                     mInfo.getLogMessageForNetworkError(networkUsable));
313         }
314     }
315
316     /**
317      * Transfer as much data as possible from the HTTP response to the
318      * destination file.
319      */
320     private void transferData(State state, InnerState innerState, InputStream in, OutputStream out)
321             throws StopRequestException {
322         final byte data[] = new byte[Constants.BUFFER_SIZE];
323         for (;;) {
324             int bytesRead = readFromResponse(state, innerState, data, in);
325             if (bytesRead == -1) { // success, end of stream already reached
326                 handleEndOfStream(state, innerState);
327                 return;
328             }
329
330             state.mGotData = true;
331             writeDataToDestination(state, data, bytesRead, out);
332             state.mCurrentBytes += bytesRead;
333             reportProgress(state, innerState);
334
335             if (Constants.LOGVV) {
336                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
337                       + mInfo.mUri);
338             }
339
340             checkPausedOrCanceled(state);
341         }
342     }
343
344     /**
345      * Called after a successful completion to take any necessary action on the downloaded file.
346      */
347     private void finalizeDestinationFile(State state) {
348         if (state.mFilename != null) {
349             // make sure the file is readable
350             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
351             syncDestination(state);
352         }
353     }
354
355     /**
356      * Called just before the thread finishes, regardless of status, to take any necessary action on
357      * the downloaded file.
358      */
359     private void cleanupDestination(State state, int finalStatus) {
360         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
361             if (Constants.LOGVV) {
362                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
363             }
364             new File(state.mFilename).delete();
365             state.mFilename = null;
366         }
367     }
368
369     /**
370      * Sync the destination file to storage.
371      */
372     private void syncDestination(State state) {
373         FileOutputStream downloadedFileStream = null;
374         try {
375             downloadedFileStream = new FileOutputStream(state.mFilename, true);
376             downloadedFileStream.getFD().sync();
377         } catch (FileNotFoundException ex) {
378             Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
379         } catch (SyncFailedException ex) {
380             Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
381         } catch (IOException ex) {
382             Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
383         } catch (RuntimeException ex) {
384             Log.w(Constants.TAG, "exception while syncing file: ", ex);
385         } finally {
386             IoUtils.closeQuietly(downloadedFileStream);
387         }
388     }
389
390     /**
391      * Check if the download has been paused or canceled, stopping the request appropriately if it
392      * has been.
393      */
394     private void checkPausedOrCanceled(State state) throws StopRequestException {
395         synchronized (mInfo) {
396             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
397                 throw new StopRequestException(
398                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
399             }
400             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
401                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
402             }
403         }
404
405         // if policy has been changed, trigger connectivity check
406         if (mPolicyDirty) {
407             checkConnectivity();
408         }
409     }
410
411     /**
412      * Report download progress through the database if necessary.
413      */
414     private void reportProgress(State state, InnerState innerState) {
415         final long now = SystemClock.elapsedRealtime();
416
417         final long sampleDelta = now - state.mSpeedSampleStart;
418         if (sampleDelta > 500) {
419             final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
420                     / sampleDelta;
421
422             if (state.mSpeed == 0) {
423                 state.mSpeed = sampleSpeed;
424             } else {
425                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
426             }
427
428             state.mSpeedSampleStart = now;
429             state.mSpeedSampleBytes = state.mCurrentBytes;
430
431             DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
432         }
433
434         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
435             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
436             ContentValues values = new ContentValues();
437             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
438             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
439             state.mBytesNotified = state.mCurrentBytes;
440             state.mTimeLastNotification = now;
441         }
442     }
443
444     /**
445      * Write a data buffer to the destination file.
446      * @param data buffer containing the data to write
447      * @param bytesRead how many bytes to write from the buffer
448      */
449     private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
450             throws StopRequestException {
451         mStorageManager.verifySpaceBeforeWritingToFile(
452                 mInfo.mDestination, state.mFilename, bytesRead);
453
454         boolean forceVerified = false;
455         while (true) {
456             try {
457                 out.write(data, 0, bytesRead);
458                 return;
459             } catch (IOException ex) {
460                 // TODO: better differentiate between DRM and disk failures
461                 if (!forceVerified) {
462                     // couldn't write to file. are we out of space? check.
463                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
464                     forceVerified = true;
465                 } else {
466                     throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
467                             "Failed to write data: " + ex);
468                 }
469             }
470         }
471     }
472
473     /**
474      * Called when we've reached the end of the HTTP response stream, to update the database and
475      * check for consistency.
476      */
477     private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
478         ContentValues values = new ContentValues();
479         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
480         if (innerState.mContentLength == -1) {
481             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
482         }
483         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
484
485         boolean lengthMismatched = (innerState.mContentLength != -1)
486                 && (state.mCurrentBytes != innerState.mContentLength);
487         if (lengthMismatched) {
488             if (cannotResume(state)) {
489                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
490                         "mismatched content length; unable to resume");
491             } else {
492                 throw new StopRequestException(getFinalStatusForHttpError(state),
493                         "closed socket before end of file");
494             }
495         }
496     }
497
498     private boolean cannotResume(State state) {
499         return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
500                 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
501     }
502
503     /**
504      * Read some data from the HTTP response stream, handling I/O errors.
505      * @param data buffer to use to read data
506      * @param entityStream stream for reading the HTTP response entity
507      * @return the number of bytes actually read or -1 if the end of the stream has been reached
508      */
509     private int readFromResponse(State state, InnerState innerState, byte[] data,
510                                  InputStream entityStream) throws StopRequestException {
511         try {
512             return entityStream.read(data);
513         } catch (IOException ex) {
514             ContentValues values = new ContentValues();
515             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
516             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
517             if (cannotResume(state)) {
518                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
519                         "Failed reading response: " + ex + "; unable to resume", ex);
520             } else {
521                 throw new StopRequestException(getFinalStatusForHttpError(state),
522                         "Failed reading response: " + ex, ex);
523             }
524         }
525     }
526
527     /**
528      * Read HTTP response headers and take appropriate action, including setting up the destination
529      * file and updating the database.
530      */
531     private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection conn)
532             throws StopRequestException {
533         if (state.mContinuingDownload) {
534             // ignore response headers on resume requests
535             return;
536         }
537
538         readResponseHeaders(state, innerState, conn);
539
540         state.mFilename = Helpers.generateSaveFile(
541                 mContext,
542                 mInfo.mUri,
543                 mInfo.mHint,
544                 innerState.mContentDisposition,
545                 innerState.mContentLocation,
546                 state.mMimeType,
547                 mInfo.mDestination,
548                 innerState.mContentLength,
549                 mInfo.mIsPublicApi, mStorageManager);
550
551         updateDatabaseFromHeaders(state, innerState);
552         // check connectivity again now that we know the total size
553         checkConnectivity();
554     }
555
556     /**
557      * Update necessary database fields based on values of HTTP response headers that have been
558      * read.
559      */
560     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
561         ContentValues values = new ContentValues();
562         values.put(Downloads.Impl._DATA, state.mFilename);
563         if (state.mHeaderETag != null) {
564             values.put(Constants.ETAG, state.mHeaderETag);
565         }
566         if (state.mMimeType != null) {
567             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
568         }
569         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
570         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
571     }
572
573     /**
574      * Read headers from the HTTP response and store them into local state.
575      */
576     private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection conn)
577             throws StopRequestException {
578         innerState.mContentDisposition = conn.getHeaderField("Content-Disposition");
579         innerState.mContentLocation = conn.getHeaderField("Content-Location");
580
581         if (state.mMimeType == null) {
582             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
583         }
584
585         state.mHeaderETag = conn.getHeaderField("ETag");
586
587         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
588         if (transferEncoding == null) {
589             innerState.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
590         } else {
591             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
592             innerState.mContentLength = -1;
593         }
594
595         state.mTotalBytes = innerState.mContentLength;
596         mInfo.mTotalBytes = innerState.mContentLength;
597
598         final boolean noSizeInfo = innerState.mContentLength == -1
599                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
600         if (!mInfo.mNoIntegrity && noSizeInfo) {
601             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
602                     "can't know size of download, giving up");
603         }
604     }
605
606     /**
607      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
608      */
609     private void handleExceptionalStatus(
610             State state, InnerState innerState, HttpURLConnection conn, int statusCode)
611             throws StopRequestException {
612         if (statusCode == HTTP_UNAVAILABLE && mInfo.mNumFailed < Constants.MAX_RETRIES) {
613             handleServiceUnavailable(state, conn);
614         }
615
616         if (Constants.LOGV) {
617             Log.i(Constants.TAG, "recevd_status = " + statusCode +
618                     ", mContinuingDownload = " + state.mContinuingDownload);
619         }
620         int expectedStatus = state.mContinuingDownload ? HTTP_PARTIAL : HTTP_OK;
621         if (statusCode != expectedStatus) {
622             handleOtherStatus(state, innerState, statusCode);
623         }
624     }
625
626     /**
627      * Handle a status that we don't know how to deal with properly.
628      */
629     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
630             throws StopRequestException {
631         if (statusCode == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
632             // range request failed. it should never fail.
633             throw new IllegalStateException("Http Range request failure: totalBytes = " +
634                     state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
635         }
636         int finalStatus;
637         if (statusCode >= 400 && statusCode < 600) {
638             finalStatus = statusCode;
639         } else if (statusCode >= 300 && statusCode < 400) {
640             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
641         } else if (state.mContinuingDownload && statusCode == HTTP_OK) {
642             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
643         } else {
644             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
645         }
646         throw new StopRequestException(finalStatus, "http error " +
647                 statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
648     }
649
650     /**
651      * Handle a 503 Service Unavailable status by processing the Retry-After header.
652      */
653     private void handleServiceUnavailable(State state, HttpURLConnection conn)
654             throws StopRequestException {
655         state.mCountRetry = true;
656         state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
657         if (state.mRetryAfter < 0) {
658             state.mRetryAfter = 0;
659         } else {
660             if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
661                 state.mRetryAfter = Constants.MIN_RETRY_AFTER;
662             } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
663                 state.mRetryAfter = Constants.MAX_RETRY_AFTER;
664             }
665             state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
666             state.mRetryAfter *= 1000;
667         }
668
669         throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
670                 "got 503 Service Unavailable, will retry later");
671     }
672
673     private int getFinalStatusForHttpError(State state) {
674         int networkUsable = mInfo.checkCanUseNetwork();
675         if (networkUsable != DownloadInfo.NETWORK_OK) {
676             switch (networkUsable) {
677                 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
678                 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
679                     return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
680                 default:
681                     return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
682             }
683         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
684             state.mCountRetry = true;
685             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
686         } else {
687             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
688             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
689         }
690     }
691
692     /**
693      * Prepare the destination file to receive data.  If the file already exists, we'll set up
694      * appropriately for resumption.
695      */
696     private void setupDestinationFile(State state, InnerState innerState)
697             throws StopRequestException {
698         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
699             if (Constants.LOGV) {
700                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
701                         ", and state.mFilename: " + state.mFilename);
702             }
703             if (!Helpers.isFilenameValid(state.mFilename,
704                     mStorageManager.getDownloadDataDirectory())) {
705                 // this should never happen
706                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
707                         "found invalid internal destination filename");
708             }
709             // We're resuming a download that got interrupted
710             File f = new File(state.mFilename);
711             if (f.exists()) {
712                 if (Constants.LOGV) {
713                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
714                             ", and state.mFilename: " + state.mFilename);
715                 }
716                 long fileLength = f.length();
717                 if (fileLength == 0) {
718                     // The download hadn't actually started, we can restart from scratch
719                     if (Constants.LOGVV) {
720                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
721                                 + state.mFilename);
722                     }
723                     f.delete();
724                     state.mFilename = null;
725                     if (Constants.LOGV) {
726                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
727                                 ", BUT starting from scratch again: ");
728                     }
729                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
730                     // This should've been caught upon failure
731                     if (Constants.LOGVV) {
732                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
733                                 + state.mFilename);
734                     }
735                     f.delete();
736                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
737                             "Trying to resume a download that can't be resumed");
738                 } else {
739                     // All right, we'll be able to resume this download
740                     if (Constants.LOGV) {
741                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
742                                 ", and starting with file of length: " + fileLength);
743                     }
744                     state.mCurrentBytes = (int) fileLength;
745                     if (mInfo.mTotalBytes != -1) {
746                         innerState.mContentLength = mInfo.mTotalBytes;
747                     }
748                     state.mHeaderETag = mInfo.mETag;
749                     state.mContinuingDownload = true;
750                     if (Constants.LOGV) {
751                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
752                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
753                                 ", and setting mContinuingDownload to true: ");
754                     }
755                 }
756             }
757         }
758     }
759
760     /**
761      * Add custom headers for this download to the HTTP request.
762      */
763     private void addRequestHeaders(State state, HttpURLConnection conn) {
764         conn.addRequestProperty("User-Agent", userAgent());
765
766         for (Pair<String, String> header : mInfo.getHeaders()) {
767             conn.addRequestProperty(header.first, header.second);
768         }
769
770         if (state.mContinuingDownload) {
771             if (state.mHeaderETag != null) {
772                 conn.addRequestProperty("If-Match", state.mHeaderETag);
773             }
774             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
775             if (Constants.LOGV) {
776                 Log.i(Constants.TAG, "Adding Range header: " +
777                         "bytes=" + state.mCurrentBytes + "-");
778                 Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
779             }
780         }
781     }
782
783     /**
784      * Stores information about the completed download, and notifies the initiating application.
785      */
786     private void notifyDownloadCompleted(int status, boolean countRetry, int retryAfter,
787             boolean gotData, String filename, String mimeType, String errorMsg) {
788         notifyThroughDatabase(
789                 status, countRetry, retryAfter, gotData, filename, mimeType, errorMsg);
790         if (Downloads.Impl.isStatusCompleted(status)) {
791             mInfo.sendIntentIfRequested();
792         }
793     }
794
795     private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter,
796             boolean gotData, String filename, String mimeType, String errorMsg) {
797         ContentValues values = new ContentValues();
798         values.put(Downloads.Impl.COLUMN_STATUS, status);
799         values.put(Downloads.Impl._DATA, filename);
800         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
801         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
802         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
803         if (!countRetry) {
804             values.put(Constants.FAILED_CONNECTIONS, 0);
805         } else if (gotData) {
806             values.put(Constants.FAILED_CONNECTIONS, 1);
807         } else {
808             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
809         }
810         // save the error message. could be useful to developers.
811         if (!TextUtils.isEmpty(errorMsg)) {
812             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
813         }
814         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
815     }
816
817     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
818         @Override
819         public void onUidRulesChanged(int uid, int uidRules) {
820             // caller is NPMS, since we only register with them
821             if (uid == mInfo.mUid) {
822                 mPolicyDirty = true;
823             }
824         }
825
826         @Override
827         public void onMeteredIfacesChanged(String[] meteredIfaces) {
828             // caller is NPMS, since we only register with them
829             mPolicyDirty = true;
830         }
831
832         @Override
833         public void onRestrictBackgroundChanged(boolean restrictBackground) {
834             // caller is NPMS, since we only register with them
835             mPolicyDirty = true;
836         }
837     };
838
839     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
840         try {
841             return Long.parseLong(conn.getHeaderField(field));
842         } catch (NumberFormatException e) {
843             return defaultValue;
844         }
845     }
846 }