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