Clean up DownloadManager threading tests.
[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                 handleExceptionalStatus(state, conn, statusCode);
265                 processResponseHeaders(state, conn);
266                 in = conn.getInputStream();
267             } catch (IOException e) {
268                 throw new StopRequestException(
269                         getFinalStatusForHttpError(state), "Request failed: " + e, e);
270             }
271
272             try {
273                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
274                     drmClient = new DrmManagerClient(mContext);
275                     final RandomAccessFile file = new RandomAccessFile(
276                             new File(state.mFilename), "rw");
277                     out = new DrmOutputStream(drmClient, file, state.mMimeType);
278                     outFd = file.getFD();
279                 } else {
280                     out = new FileOutputStream(state.mFilename, true);
281                     outFd = ((FileOutputStream) out).getFD();
282                 }
283             } catch (IOException e) {
284                 throw new StopRequestException(
285                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to open destination: " + e, e);
286             }
287
288             transferData(state, in, out);
289
290             try {
291                 if (out instanceof DrmOutputStream) {
292                     ((DrmOutputStream) out).finish();
293                 }
294             } catch (IOException e) {
295                 throw new StopRequestException(
296                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to finish: " + e, e);
297             }
298
299         } finally {
300             if (drmClient != null) {
301                 drmClient.release();
302             }
303
304             IoUtils.closeQuietly(in);
305
306             try {
307                 if (out != null) out.flush();
308                 if (outFd != null) outFd.sync();
309             } catch (IOException e) {
310             } finally {
311                 IoUtils.closeQuietly(out);
312             }
313         }
314     }
315
316     /**
317      * Check if current connectivity is valid for this request.
318      */
319     private void checkConnectivity() throws StopRequestException {
320         // checking connectivity will apply current policy
321         mPolicyDirty = false;
322
323         int networkUsable = mInfo.checkCanUseNetwork();
324         if (networkUsable != DownloadInfo.NETWORK_OK) {
325             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
326             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
327                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
328                 mInfo.notifyPauseDueToSize(true);
329             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
330                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
331                 mInfo.notifyPauseDueToSize(false);
332             }
333             throw new StopRequestException(status,
334                     mInfo.getLogMessageForNetworkError(networkUsable));
335         }
336     }
337
338     /**
339      * Transfer as much data as possible from the HTTP response to the
340      * destination file.
341      */
342     private void transferData(State state, InputStream in, OutputStream out)
343             throws StopRequestException {
344         final byte data[] = new byte[Constants.BUFFER_SIZE];
345         for (;;) {
346             int bytesRead = readFromResponse(state, data, in);
347             if (bytesRead == -1) { // success, end of stream already reached
348                 handleEndOfStream(state);
349                 return;
350             }
351
352             state.mGotData = true;
353             writeDataToDestination(state, data, bytesRead, out);
354             state.mCurrentBytes += bytesRead;
355             reportProgress(state);
356
357             if (Constants.LOGVV) {
358                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
359                       + mInfo.mUri);
360             }
361
362             checkPausedOrCanceled(state);
363         }
364     }
365
366     /**
367      * Called after a successful completion to take any necessary action on the downloaded file.
368      */
369     private void finalizeDestinationFile(State state) {
370         if (state.mFilename != null) {
371             // make sure the file is readable
372             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
373         }
374     }
375
376     /**
377      * Called just before the thread finishes, regardless of status, to take any necessary action on
378      * the downloaded file.
379      */
380     private void cleanupDestination(State state, int finalStatus) {
381         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
382             if (Constants.LOGVV) {
383                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
384             }
385             new File(state.mFilename).delete();
386             state.mFilename = null;
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) {
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) throws StopRequestException {
478         ContentValues values = new ContentValues();
479         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
480         if (state.mContentLength == -1) {
481             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
482         }
483         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
484
485         final boolean lengthMismatched = (state.mContentLength != -1)
486                 && (state.mCurrentBytes != state.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, byte[] data, InputStream entityStream)
510             throws StopRequestException {
511         try {
512             return entityStream.read(data);
513         } catch (IOException ex) {
514             // TODO: handle stream errors the same as other retries
515             if ("unexpected end of stream".equals(ex.getMessage())) {
516                 return -1;
517             }
518
519             ContentValues values = new ContentValues();
520             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
521             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
522             if (cannotResume(state)) {
523                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
524                         "Failed reading response: " + ex + "; unable to resume", ex);
525             } else {
526                 throw new StopRequestException(getFinalStatusForHttpError(state),
527                         "Failed reading response: " + ex, ex);
528             }
529         }
530     }
531
532     /**
533      * Read HTTP response headers and take appropriate action, including setting up the destination
534      * file and updating the database.
535      */
536     private void processResponseHeaders(State state, HttpURLConnection conn)
537             throws StopRequestException {
538         if (state.mContinuingDownload) {
539             // ignore response headers on resume requests
540             return;
541         }
542
543         readResponseHeaders(state, conn);
544
545         state.mFilename = Helpers.generateSaveFile(
546                 mContext,
547                 mInfo.mUri,
548                 mInfo.mHint,
549                 state.mContentDisposition,
550                 state.mContentLocation,
551                 state.mMimeType,
552                 mInfo.mDestination,
553                 state.mContentLength,
554                 mInfo.mIsPublicApi, mStorageManager);
555
556         updateDatabaseFromHeaders(state);
557         // check connectivity again now that we know the total size
558         checkConnectivity();
559     }
560
561     /**
562      * Update necessary database fields based on values of HTTP response headers that have been
563      * read.
564      */
565     private void updateDatabaseFromHeaders(State state) {
566         ContentValues values = new ContentValues();
567         values.put(Downloads.Impl._DATA, state.mFilename);
568         if (state.mHeaderETag != null) {
569             values.put(Constants.ETAG, state.mHeaderETag);
570         }
571         if (state.mMimeType != null) {
572             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
573         }
574         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
575         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
576     }
577
578     /**
579      * Read headers from the HTTP response and store them into local state.
580      */
581     private void readResponseHeaders(State state, HttpURLConnection conn)
582             throws StopRequestException {
583         state.mContentDisposition = conn.getHeaderField("Content-Disposition");
584         state.mContentLocation = conn.getHeaderField("Content-Location");
585
586         if (state.mMimeType == null) {
587             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
588         }
589
590         state.mHeaderETag = conn.getHeaderField("ETag");
591
592         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
593         if (transferEncoding == null) {
594             state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
595         } else {
596             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
597             state.mContentLength = -1;
598         }
599
600         state.mTotalBytes = state.mContentLength;
601         mInfo.mTotalBytes = state.mContentLength;
602
603         final boolean noSizeInfo = state.mContentLength == -1
604                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
605         if (!mInfo.mNoIntegrity && noSizeInfo) {
606             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
607                     "can't know size of download, giving up");
608         }
609     }
610
611     /**
612      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
613      */
614     private void handleExceptionalStatus(State state, HttpURLConnection conn, int statusCode)
615             throws StopRequestException {
616         if (statusCode == HTTP_UNAVAILABLE && mInfo.mNumFailed < Constants.MAX_RETRIES) {
617             handleServiceUnavailable(state, conn);
618         }
619
620         if (Constants.LOGV) {
621             Log.i(Constants.TAG, "recevd_status = " + statusCode +
622                     ", mContinuingDownload = " + state.mContinuingDownload);
623         }
624         int expectedStatus = state.mContinuingDownload ? HTTP_PARTIAL : HTTP_OK;
625         if (statusCode != expectedStatus) {
626             handleOtherStatus(state, statusCode);
627         }
628     }
629
630     /**
631      * Handle a status that we don't know how to deal with properly.
632      */
633     private void handleOtherStatus(State state, int statusCode) throws StopRequestException {
634         if (statusCode == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
635             // range request failed. it should never fail.
636             throw new IllegalStateException("Http Range request failure: totalBytes = " +
637                     state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
638         }
639         int finalStatus;
640         if (statusCode >= 400 && statusCode < 600) {
641             finalStatus = statusCode;
642         } else if (statusCode >= 300 && statusCode < 400) {
643             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
644         } else if (state.mContinuingDownload && statusCode == HTTP_OK) {
645             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
646         } else {
647             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
648         }
649         throw new StopRequestException(finalStatus, "http error " +
650                 statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
651     }
652
653     /**
654      * Handle a 503 Service Unavailable status by processing the Retry-After header.
655      */
656     private void handleServiceUnavailable(State state, HttpURLConnection conn)
657             throws StopRequestException {
658         state.mCountRetry = true;
659         state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
660         if (state.mRetryAfter < 0) {
661             state.mRetryAfter = 0;
662         } else {
663             if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
664                 state.mRetryAfter = Constants.MIN_RETRY_AFTER;
665             } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
666                 state.mRetryAfter = Constants.MAX_RETRY_AFTER;
667             }
668             state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
669             state.mRetryAfter *= 1000;
670         }
671
672         throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
673                 "got 503 Service Unavailable, will retry later");
674     }
675
676     private int getFinalStatusForHttpError(State state) {
677         int networkUsable = mInfo.checkCanUseNetwork();
678         if (networkUsable != DownloadInfo.NETWORK_OK) {
679             switch (networkUsable) {
680                 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
681                 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
682                     return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
683                 default:
684                     return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
685             }
686         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
687             state.mCountRetry = true;
688             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
689         } else {
690             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
691             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
692         }
693     }
694
695     /**
696      * Prepare the destination file to receive data.  If the file already exists, we'll set up
697      * appropriately for resumption.
698      */
699     private void setupDestinationFile(State state) throws StopRequestException {
700         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
701             if (Constants.LOGV) {
702                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
703                         ", and state.mFilename: " + state.mFilename);
704             }
705             if (!Helpers.isFilenameValid(state.mFilename,
706                     mStorageManager.getDownloadDataDirectory())) {
707                 // this should never happen
708                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
709                         "found invalid internal destination filename");
710             }
711             // We're resuming a download that got interrupted
712             File f = new File(state.mFilename);
713             if (f.exists()) {
714                 if (Constants.LOGV) {
715                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
716                             ", and state.mFilename: " + state.mFilename);
717                 }
718                 long fileLength = f.length();
719                 if (fileLength == 0) {
720                     // The download hadn't actually started, we can restart from scratch
721                     if (Constants.LOGVV) {
722                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
723                                 + state.mFilename);
724                     }
725                     f.delete();
726                     state.mFilename = null;
727                     if (Constants.LOGV) {
728                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
729                                 ", BUT starting from scratch again: ");
730                     }
731                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
732                     // This should've been caught upon failure
733                     if (Constants.LOGVV) {
734                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
735                                 + state.mFilename);
736                     }
737                     f.delete();
738                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
739                             "Trying to resume a download that can't be resumed");
740                 } else {
741                     // All right, we'll be able to resume this download
742                     if (Constants.LOGV) {
743                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
744                                 ", and starting with file of length: " + fileLength);
745                     }
746                     state.mCurrentBytes = (int) fileLength;
747                     if (mInfo.mTotalBytes != -1) {
748                         state.mContentLength = mInfo.mTotalBytes;
749                     }
750                     state.mHeaderETag = mInfo.mETag;
751                     state.mContinuingDownload = true;
752                     if (Constants.LOGV) {
753                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
754                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
755                                 ", and setting mContinuingDownload to true: ");
756                     }
757                 }
758             }
759         }
760     }
761
762     /**
763      * Add custom headers for this download to the HTTP request.
764      */
765     private void addRequestHeaders(State state, HttpURLConnection conn) {
766         conn.addRequestProperty("User-Agent", userAgent());
767
768         for (Pair<String, String> header : mInfo.getHeaders()) {
769             conn.addRequestProperty(header.first, header.second);
770         }
771
772         if (state.mContinuingDownload) {
773             if (state.mHeaderETag != null) {
774                 conn.addRequestProperty("If-Match", state.mHeaderETag);
775             }
776             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
777             if (Constants.LOGV) {
778                 Log.i(Constants.TAG, "Adding Range header: " +
779                         "bytes=" + state.mCurrentBytes + "-");
780                 Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
781             }
782         }
783     }
784
785     /**
786      * Stores information about the completed download, and notifies the initiating application.
787      */
788     private void notifyDownloadCompleted(int status, boolean countRetry, int retryAfter,
789             boolean gotData, String filename, String mimeType, String errorMsg) {
790         notifyThroughDatabase(
791                 status, countRetry, retryAfter, gotData, filename, mimeType, errorMsg);
792         if (Downloads.Impl.isStatusCompleted(status)) {
793             mInfo.sendIntentIfRequested();
794         }
795     }
796
797     private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter,
798             boolean gotData, String filename, String mimeType, String errorMsg) {
799         ContentValues values = new ContentValues();
800         values.put(Downloads.Impl.COLUMN_STATUS, status);
801         values.put(Downloads.Impl._DATA, filename);
802         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
803         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
804         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
805         if (!countRetry) {
806             values.put(Constants.FAILED_CONNECTIONS, 0);
807         } else if (gotData) {
808             values.put(Constants.FAILED_CONNECTIONS, 1);
809         } else {
810             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
811         }
812         // save the error message. could be useful to developers.
813         if (!TextUtils.isEmpty(errorMsg)) {
814             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
815         }
816         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
817     }
818
819     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
820         @Override
821         public void onUidRulesChanged(int uid, int uidRules) {
822             // caller is NPMS, since we only register with them
823             if (uid == mInfo.mUid) {
824                 mPolicyDirty = true;
825             }
826         }
827
828         @Override
829         public void onMeteredIfacesChanged(String[] meteredIfaces) {
830             // caller is NPMS, since we only register with them
831             mPolicyDirty = true;
832         }
833
834         @Override
835         public void onRestrictBackgroundChanged(boolean restrictBackground) {
836             // caller is NPMS, since we only register with them
837             mPolicyDirty = true;
838         }
839     };
840
841     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
842         try {
843             return Long.parseLong(conn.getHeaderField(field));
844         } catch (NumberFormatException e) {
845             return defaultValue;
846         }
847     }
848 }