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