Always append to files, handle end of stream.
[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 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         DrmManagerClient drmClient = null;
255         InputStream in = null;
256         OutputStream out = null;
257         FileDescriptor outFd = null;
258         try {
259             try {
260                 // Asking for response code will execute the request
261                 final int statusCode = conn.getResponseCode();
262                 in = conn.getInputStream();
263
264                 handleExceptionalStatus(state, innerState, conn, statusCode);
265                 processResponseHeaders(state, innerState, conn);
266             } catch (IOException e) {
267                 throw new StopRequestException(
268                         getFinalStatusForHttpError(state), "Request failed: " + e, e);
269             }
270
271             try {
272                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
273                     drmClient = new DrmManagerClient(mContext);
274                     final RandomAccessFile file = new RandomAccessFile(
275                             new File(state.mFilename), "rw");
276                     out = new DrmOutputStream(drmClient, file, state.mMimeType);
277                     outFd = file.getFD();
278                 } else {
279                     out = new FileOutputStream(state.mFilename, true);
280                     outFd = ((FileOutputStream) out).getFD();
281                 }
282             } catch (IOException e) {
283                 throw new StopRequestException(
284                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to open destination: " + e, e);
285             }
286
287             transferData(state, innerState, in, out);
288
289             try {
290                 if (out instanceof DrmOutputStream) {
291                     ((DrmOutputStream) out).finish();
292                 }
293             } catch (IOException e) {
294                 throw new StopRequestException(
295                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to finish: " + e, e);
296             }
297
298         } finally {
299             if (drmClient != null) {
300                 drmClient.release();
301             }
302
303             IoUtils.closeQuietly(in);
304
305             try {
306                 if (out != null) out.flush();
307                 if (outFd != null) outFd.sync();
308             } catch (IOException e) {
309             } finally {
310                 IoUtils.closeQuietly(out);
311             }
312         }
313     }
314
315     /**
316      * Check if current connectivity is valid for this request.
317      */
318     private void checkConnectivity() throws StopRequestException {
319         // checking connectivity will apply current policy
320         mPolicyDirty = false;
321
322         int networkUsable = mInfo.checkCanUseNetwork();
323         if (networkUsable != DownloadInfo.NETWORK_OK) {
324             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
325             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
326                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
327                 mInfo.notifyPauseDueToSize(true);
328             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
329                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
330                 mInfo.notifyPauseDueToSize(false);
331             }
332             throw new StopRequestException(status,
333                     mInfo.getLogMessageForNetworkError(networkUsable));
334         }
335     }
336
337     /**
338      * Transfer as much data as possible from the HTTP response to the
339      * destination file.
340      */
341     private void transferData(State state, InnerState innerState, InputStream in, OutputStream out)
342             throws StopRequestException {
343         final byte data[] = new byte[Constants.BUFFER_SIZE];
344         for (;;) {
345             int bytesRead = readFromResponse(state, innerState, data, in);
346             if (bytesRead == -1) { // success, end of stream already reached
347                 handleEndOfStream(state, innerState);
348                 return;
349             }
350
351             state.mGotData = true;
352             writeDataToDestination(state, data, bytesRead, out);
353             state.mCurrentBytes += bytesRead;
354             reportProgress(state, innerState);
355
356             if (Constants.LOGVV) {
357                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
358                       + mInfo.mUri);
359             }
360
361             checkPausedOrCanceled(state);
362         }
363     }
364
365     /**
366      * Called after a successful completion to take any necessary action on the downloaded file.
367      */
368     private void finalizeDestinationFile(State state) {
369         if (state.mFilename != null) {
370             // make sure the file is readable
371             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
372         }
373     }
374
375     /**
376      * Called just before the thread finishes, regardless of status, to take any necessary action on
377      * the downloaded file.
378      */
379     private void cleanupDestination(State state, int finalStatus) {
380         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
381             if (Constants.LOGVV) {
382                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
383             }
384             new File(state.mFilename).delete();
385             state.mFilename = null;
386         }
387     }
388
389     /**
390      * Check if the download has been paused or canceled, stopping the request appropriately if it
391      * has been.
392      */
393     private void checkPausedOrCanceled(State state) throws StopRequestException {
394         synchronized (mInfo) {
395             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
396                 throw new StopRequestException(
397                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
398             }
399             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
400                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
401             }
402         }
403
404         // if policy has been changed, trigger connectivity check
405         if (mPolicyDirty) {
406             checkConnectivity();
407         }
408     }
409
410     /**
411      * Report download progress through the database if necessary.
412      */
413     private void reportProgress(State state, InnerState innerState) {
414         final long now = SystemClock.elapsedRealtime();
415
416         final long sampleDelta = now - state.mSpeedSampleStart;
417         if (sampleDelta > 500) {
418             final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
419                     / sampleDelta;
420
421             if (state.mSpeed == 0) {
422                 state.mSpeed = sampleSpeed;
423             } else {
424                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
425             }
426
427             state.mSpeedSampleStart = now;
428             state.mSpeedSampleBytes = state.mCurrentBytes;
429
430             DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
431         }
432
433         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
434             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
435             ContentValues values = new ContentValues();
436             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
437             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
438             state.mBytesNotified = state.mCurrentBytes;
439             state.mTimeLastNotification = now;
440         }
441     }
442
443     /**
444      * Write a data buffer to the destination file.
445      * @param data buffer containing the data to write
446      * @param bytesRead how many bytes to write from the buffer
447      */
448     private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
449             throws StopRequestException {
450         mStorageManager.verifySpaceBeforeWritingToFile(
451                 mInfo.mDestination, state.mFilename, bytesRead);
452
453         boolean forceVerified = false;
454         while (true) {
455             try {
456                 out.write(data, 0, bytesRead);
457                 return;
458             } catch (IOException ex) {
459                 // TODO: better differentiate between DRM and disk failures
460                 if (!forceVerified) {
461                     // couldn't write to file. are we out of space? check.
462                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
463                     forceVerified = true;
464                 } else {
465                     throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
466                             "Failed to write data: " + ex);
467                 }
468             }
469         }
470     }
471
472     /**
473      * Called when we've reached the end of the HTTP response stream, to update the database and
474      * check for consistency.
475      */
476     private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
477         ContentValues values = new ContentValues();
478         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
479         if (innerState.mContentLength == -1) {
480             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
481         }
482         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
483
484         boolean lengthMismatched = (innerState.mContentLength != -1)
485                 && (state.mCurrentBytes != innerState.mContentLength);
486         if (lengthMismatched) {
487             if (cannotResume(state)) {
488                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
489                         "mismatched content length; unable to resume");
490             } else {
491                 throw new StopRequestException(getFinalStatusForHttpError(state),
492                         "closed socket before end of file");
493             }
494         }
495     }
496
497     private boolean cannotResume(State state) {
498         return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
499                 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
500     }
501
502     /**
503      * Read some data from the HTTP response stream, handling I/O errors.
504      * @param data buffer to use to read data
505      * @param entityStream stream for reading the HTTP response entity
506      * @return the number of bytes actually read or -1 if the end of the stream has been reached
507      */
508     private int readFromResponse(State state, InnerState innerState, byte[] data,
509                                  InputStream entityStream) throws StopRequestException {
510         try {
511             return entityStream.read(data);
512         } catch (IOException ex) {
513             // TODO: handle stream errors the same as other retries
514             if ("unexpected end of stream".equals(ex.getMessage())) {
515                 return -1;
516             }
517
518             ContentValues values = new ContentValues();
519             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
520             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
521             if (cannotResume(state)) {
522                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
523                         "Failed reading response: " + ex + "; unable to resume", ex);
524             } else {
525                 throw new StopRequestException(getFinalStatusForHttpError(state),
526                         "Failed reading response: " + ex, ex);
527             }
528         }
529     }
530
531     /**
532      * Read HTTP response headers and take appropriate action, including setting up the destination
533      * file and updating the database.
534      */
535     private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection conn)
536             throws StopRequestException {
537         if (state.mContinuingDownload) {
538             // ignore response headers on resume requests
539             return;
540         }
541
542         readResponseHeaders(state, innerState, conn);
543
544         state.mFilename = Helpers.generateSaveFile(
545                 mContext,
546                 mInfo.mUri,
547                 mInfo.mHint,
548                 innerState.mContentDisposition,
549                 innerState.mContentLocation,
550                 state.mMimeType,
551                 mInfo.mDestination,
552                 innerState.mContentLength,
553                 mInfo.mIsPublicApi, mStorageManager);
554
555         updateDatabaseFromHeaders(state, innerState);
556         // check connectivity again now that we know the total size
557         checkConnectivity();
558     }
559
560     /**
561      * Update necessary database fields based on values of HTTP response headers that have been
562      * read.
563      */
564     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
565         ContentValues values = new ContentValues();
566         values.put(Downloads.Impl._DATA, state.mFilename);
567         if (state.mHeaderETag != null) {
568             values.put(Constants.ETAG, state.mHeaderETag);
569         }
570         if (state.mMimeType != null) {
571             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
572         }
573         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
574         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
575     }
576
577     /**
578      * Read headers from the HTTP response and store them into local state.
579      */
580     private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection conn)
581             throws StopRequestException {
582         innerState.mContentDisposition = conn.getHeaderField("Content-Disposition");
583         innerState.mContentLocation = conn.getHeaderField("Content-Location");
584
585         if (state.mMimeType == null) {
586             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
587         }
588
589         state.mHeaderETag = conn.getHeaderField("ETag");
590
591         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
592         if (transferEncoding == null) {
593             innerState.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
594         } else {
595             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
596             innerState.mContentLength = -1;
597         }
598
599         state.mTotalBytes = innerState.mContentLength;
600         mInfo.mTotalBytes = innerState.mContentLength;
601
602         final boolean noSizeInfo = innerState.mContentLength == -1
603                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
604         if (!mInfo.mNoIntegrity && noSizeInfo) {
605             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
606                     "can't know size of download, giving up");
607         }
608     }
609
610     /**
611      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
612      */
613     private void handleExceptionalStatus(
614             State state, InnerState innerState, 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, innerState, 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, InnerState innerState, int statusCode)
634             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, InnerState innerState)
701             throws StopRequestException {
702         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
703             if (Constants.LOGV) {
704                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
705                         ", and state.mFilename: " + state.mFilename);
706             }
707             if (!Helpers.isFilenameValid(state.mFilename,
708                     mStorageManager.getDownloadDataDirectory())) {
709                 // this should never happen
710                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
711                         "found invalid internal destination filename");
712             }
713             // We're resuming a download that got interrupted
714             File f = new File(state.mFilename);
715             if (f.exists()) {
716                 if (Constants.LOGV) {
717                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
718                             ", and state.mFilename: " + state.mFilename);
719                 }
720                 long fileLength = f.length();
721                 if (fileLength == 0) {
722                     // The download hadn't actually started, we can restart from scratch
723                     if (Constants.LOGVV) {
724                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
725                                 + state.mFilename);
726                     }
727                     f.delete();
728                     state.mFilename = null;
729                     if (Constants.LOGV) {
730                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
731                                 ", BUT starting from scratch again: ");
732                     }
733                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
734                     // This should've been caught upon failure
735                     if (Constants.LOGVV) {
736                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
737                                 + state.mFilename);
738                     }
739                     f.delete();
740                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
741                             "Trying to resume a download that can't be resumed");
742                 } else {
743                     // All right, we'll be able to resume this download
744                     if (Constants.LOGV) {
745                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
746                                 ", and starting with file of length: " + fileLength);
747                     }
748                     state.mCurrentBytes = (int) fileLength;
749                     if (mInfo.mTotalBytes != -1) {
750                         innerState.mContentLength = mInfo.mTotalBytes;
751                     }
752                     state.mHeaderETag = mInfo.mETag;
753                     state.mContinuingDownload = true;
754                     if (Constants.LOGV) {
755                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
756                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
757                                 ", and setting mContinuingDownload to true: ");
758                     }
759                 }
760             }
761         }
762     }
763
764     /**
765      * Add custom headers for this download to the HTTP request.
766      */
767     private void addRequestHeaders(State state, HttpURLConnection conn) {
768         conn.addRequestProperty("User-Agent", userAgent());
769
770         for (Pair<String, String> header : mInfo.getHeaders()) {
771             conn.addRequestProperty(header.first, header.second);
772         }
773
774         if (state.mContinuingDownload) {
775             if (state.mHeaderETag != null) {
776                 conn.addRequestProperty("If-Match", state.mHeaderETag);
777             }
778             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
779             if (Constants.LOGV) {
780                 Log.i(Constants.TAG, "Adding Range header: " +
781                         "bytes=" + state.mCurrentBytes + "-");
782                 Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
783             }
784         }
785     }
786
787     /**
788      * Stores information about the completed download, and notifies the initiating application.
789      */
790     private void notifyDownloadCompleted(int status, boolean countRetry, int retryAfter,
791             boolean gotData, String filename, String mimeType, String errorMsg) {
792         notifyThroughDatabase(
793                 status, countRetry, retryAfter, gotData, filename, mimeType, errorMsg);
794         if (Downloads.Impl.isStatusCompleted(status)) {
795             mInfo.sendIntentIfRequested();
796         }
797     }
798
799     private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter,
800             boolean gotData, String filename, String mimeType, String errorMsg) {
801         ContentValues values = new ContentValues();
802         values.put(Downloads.Impl.COLUMN_STATUS, status);
803         values.put(Downloads.Impl._DATA, filename);
804         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
805         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
806         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
807         if (!countRetry) {
808             values.put(Constants.FAILED_CONNECTIONS, 0);
809         } else if (gotData) {
810             values.put(Constants.FAILED_CONNECTIONS, 1);
811         } else {
812             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
813         }
814         // save the error message. could be useful to developers.
815         if (!TextUtils.isEmpty(errorMsg)) {
816             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
817         }
818         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
819     }
820
821     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
822         @Override
823         public void onUidRulesChanged(int uid, int uidRules) {
824             // caller is NPMS, since we only register with them
825             if (uid == mInfo.mUid) {
826                 mPolicyDirty = true;
827             }
828         }
829
830         @Override
831         public void onMeteredIfacesChanged(String[] meteredIfaces) {
832             // caller is NPMS, since we only register with them
833             mPolicyDirty = true;
834         }
835
836         @Override
837         public void onRestrictBackgroundChanged(boolean restrictBackground) {
838             // caller is NPMS, since we only register with them
839             mPolicyDirty = true;
840         }
841     };
842
843     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
844         try {
845             return Long.parseLong(conn.getHeaderField(field));
846         } catch (NumberFormatException e) {
847             return defaultValue;
848         }
849     }
850 }