Count download WakeLocks against requesting app.
[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.provider.Downloads.Impl.STATUS_BAD_REQUEST;
20 import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME;
21 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
22 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
23 import static android.provider.Downloads.Impl.STATUS_SUCCESS;
24 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
25 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
26 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
27 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
28 import static com.android.providers.downloads.Constants.TAG;
29 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
30 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
31 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
32 import static java.net.HttpURLConnection.HTTP_OK;
33 import static java.net.HttpURLConnection.HTTP_PARTIAL;
34 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
35 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
36
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.drm.DrmManagerClient;
41 import android.drm.DrmOutputStream;
42 import android.net.ConnectivityManager;
43 import android.net.INetworkPolicyListener;
44 import android.net.NetworkInfo;
45 import android.net.NetworkPolicyManager;
46 import android.net.TrafficStats;
47 import android.os.FileUtils;
48 import android.os.PowerManager;
49 import android.os.Process;
50 import android.os.SystemClock;
51 import android.os.WorkSource;
52 import android.provider.Downloads;
53 import android.text.TextUtils;
54 import android.util.Log;
55 import android.util.Pair;
56
57 import com.android.providers.downloads.DownloadInfo.NetworkState;
58
59 import libcore.io.IoUtils;
60
61 import java.io.File;
62 import java.io.FileDescriptor;
63 import java.io.FileOutputStream;
64 import java.io.IOException;
65 import java.io.InputStream;
66 import java.io.OutputStream;
67 import java.io.RandomAccessFile;
68 import java.net.HttpURLConnection;
69 import java.net.MalformedURLException;
70 import java.net.URL;
71 import java.net.URLConnection;
72
73 /**
74  * Task which executes a given {@link DownloadInfo}: making network requests,
75  * persisting data to disk, and updating {@link DownloadProvider}.
76  */
77 public class DownloadThread implements Runnable {
78
79     // TODO: bind each download to a specific network interface to avoid state
80     // checking races once we have ConnectivityManager API
81
82     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
83     private static final int HTTP_TEMP_REDIRECT = 307;
84
85     private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
86
87     private final Context mContext;
88     private final DownloadInfo mInfo;
89     private final SystemFacade mSystemFacade;
90     private final StorageManager mStorageManager;
91     private final DownloadNotifier mNotifier;
92
93     private volatile boolean mPolicyDirty;
94
95     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
96             StorageManager storageManager, DownloadNotifier notifier) {
97         mContext = context;
98         mSystemFacade = systemFacade;
99         mInfo = info;
100         mStorageManager = storageManager;
101         mNotifier = notifier;
102     }
103
104     /**
105      * Returns the user agent provided by the initiating app, or use the default one
106      */
107     private String userAgent() {
108         String userAgent = mInfo.mUserAgent;
109         if (userAgent == null) {
110             userAgent = Constants.DEFAULT_USER_AGENT;
111         }
112         return userAgent;
113     }
114
115     /**
116      * State for the entire run() method.
117      */
118     static class State {
119         public String mFilename;
120         public String mMimeType;
121         public int mRetryAfter = 0;
122         public boolean mGotData = false;
123         public String mRequestUri;
124         public long mTotalBytes = -1;
125         public long mCurrentBytes = 0;
126         public String mHeaderETag;
127         public boolean mContinuingDownload = false;
128         public long mBytesNotified = 0;
129         public long mTimeLastNotification = 0;
130         public int mNetworkType = ConnectivityManager.TYPE_NONE;
131
132         /** Historical bytes/second speed of this download. */
133         public long mSpeed;
134         /** Time when current sample started. */
135         public long mSpeedSampleStart;
136         /** Bytes transferred since current sample started. */
137         public long mSpeedSampleBytes;
138
139         public long mContentLength = -1;
140         public String mContentDisposition;
141         public String mContentLocation;
142
143         public int mRedirectionCount;
144         public URL mUrl;
145
146         public State(DownloadInfo info) {
147             mMimeType = Intent.normalizeMimeType(info.mMimeType);
148             mRequestUri = info.mUri;
149             mFilename = info.mFileName;
150             mTotalBytes = info.mTotalBytes;
151             mCurrentBytes = info.mCurrentBytes;
152         }
153
154         public void resetBeforeExecute() {
155             // Reset any state from previous execution
156             mContentLength = -1;
157             mContentDisposition = null;
158             mContentLocation = null;
159             mRedirectionCount = 0;
160         }
161     }
162
163     @Override
164     public void run() {
165         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
166         try {
167             runInternal();
168         } finally {
169             mNotifier.notifyDownloadSpeed(mInfo.mId, 0);
170         }
171     }
172
173     private void runInternal() {
174         // Skip when download already marked as finished; this download was
175         // probably started again while racing with UpdateThread.
176         if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
177                 == Downloads.Impl.STATUS_SUCCESS) {
178             Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
179             return;
180         }
181
182         State state = new State(mInfo);
183         PowerManager.WakeLock wakeLock = null;
184         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
185         int numFailed = mInfo.mNumFailed;
186         String errorMsg = null;
187
188         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
189         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
190
191         try {
192             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
193             wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
194             wakeLock.acquire();
195
196             // while performing download, register for rules updates
197             netPolicy.registerListener(mPolicyListener);
198
199             Log.i(Constants.TAG, "Download " + mInfo.mId + " starting");
200
201             // Remember which network this download started on; used to
202             // determine if errors were due to network changes.
203             final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
204             if (info != null) {
205                 state.mNetworkType = info.getType();
206             }
207
208             // Network traffic on this thread should be counted against the
209             // requesting UID, and is tagged with well-known value.
210             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
211             TrafficStats.setThreadStatsUid(mInfo.mUid);
212
213             try {
214                 // TODO: migrate URL sanity checking into client side of API
215                 state.mUrl = new URL(state.mRequestUri);
216             } catch (MalformedURLException e) {
217                 throw new StopRequestException(STATUS_BAD_REQUEST, e);
218             }
219
220             executeDownload(state);
221
222             finalizeDestinationFile(state);
223             finalStatus = Downloads.Impl.STATUS_SUCCESS;
224         } catch (StopRequestException error) {
225             // remove the cause before printing, in case it contains PII
226             errorMsg = error.getMessage();
227             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
228             Log.w(Constants.TAG, msg);
229             if (Constants.LOGV) {
230                 Log.w(Constants.TAG, msg, error);
231             }
232             finalStatus = error.getFinalStatus();
233
234             // Nobody below our level should request retries, since we handle
235             // failure counts at this level.
236             if (finalStatus == STATUS_WAITING_TO_RETRY) {
237                 throw new IllegalStateException("Execution should always throw final error codes");
238             }
239
240             // Some errors should be retryable, unless we fail too many times.
241             if (isStatusRetryable(finalStatus)) {
242                 if (state.mGotData) {
243                     numFailed = 1;
244                 } else {
245                     numFailed += 1;
246                 }
247
248                 if (numFailed < Constants.MAX_RETRIES) {
249                     final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
250                     if (info != null && info.getType() == state.mNetworkType
251                             && info.isConnected()) {
252                         // Underlying network is still intact, use normal backoff
253                         finalStatus = STATUS_WAITING_TO_RETRY;
254                     } else {
255                         // Network changed, retry on any next available
256                         finalStatus = STATUS_WAITING_FOR_NETWORK;
257                     }
258                 }
259             }
260
261             // fall through to finally block
262         } catch (Throwable ex) {
263             errorMsg = ex.getMessage();
264             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
265             Log.w(Constants.TAG, msg, ex);
266             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
267             // falls through to the code that reports an error
268         } finally {
269             if (finalStatus == STATUS_SUCCESS) {
270                 TrafficStats.incrementOperationCount(1);
271             }
272
273             TrafficStats.clearThreadStatsTag();
274             TrafficStats.clearThreadStatsUid();
275
276             cleanupDestination(state, finalStatus);
277             notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
278
279             Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status "
280                     + Downloads.Impl.statusToString(finalStatus));
281
282             netPolicy.unregisterListener(mPolicyListener);
283
284             if (wakeLock != null) {
285                 wakeLock.release();
286                 wakeLock = null;
287             }
288         }
289         mStorageManager.incrementNumDownloadsSoFar();
290     }
291
292     /**
293      * Fully execute a single download request. Setup and send the request,
294      * handle the response, and transfer the data to the destination file.
295      */
296     private void executeDownload(State state) throws StopRequestException {
297         state.resetBeforeExecute();
298         setupDestinationFile(state);
299
300         // skip when already finished; remove after fixing race in 5217390
301         if (state.mCurrentBytes == state.mTotalBytes) {
302             Log.i(Constants.TAG, "Skipping initiating request for download " +
303                   mInfo.mId + "; already completed");
304             return;
305         }
306
307         while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) {
308             // Open connection and follow any redirects until we have a useful
309             // response with body.
310             HttpURLConnection conn = null;
311             try {
312                 checkConnectivity();
313                 conn = (HttpURLConnection) state.mUrl.openConnection();
314                 conn.setInstanceFollowRedirects(false);
315                 conn.setConnectTimeout(DEFAULT_TIMEOUT);
316                 conn.setReadTimeout(DEFAULT_TIMEOUT);
317
318                 addRequestHeaders(state, conn);
319
320                 final int responseCode = conn.getResponseCode();
321                 switch (responseCode) {
322                     case HTTP_OK:
323                         if (state.mContinuingDownload) {
324                             throw new StopRequestException(
325                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
326                         }
327                         processResponseHeaders(state, conn);
328                         transferData(state, conn);
329                         return;
330
331                     case HTTP_PARTIAL:
332                         if (!state.mContinuingDownload) {
333                             throw new StopRequestException(
334                                     STATUS_CANNOT_RESUME, "Expected OK, but received partial");
335                         }
336                         transferData(state, conn);
337                         return;
338
339                     case HTTP_MOVED_PERM:
340                     case HTTP_MOVED_TEMP:
341                     case HTTP_SEE_OTHER:
342                     case HTTP_TEMP_REDIRECT:
343                         final String location = conn.getHeaderField("Location");
344                         state.mUrl = new URL(state.mUrl, location);
345                         if (responseCode == HTTP_MOVED_PERM) {
346                             // Push updated URL back to database
347                             state.mRequestUri = state.mUrl.toString();
348                         }
349                         continue;
350
351                     case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
352                         throw new StopRequestException(
353                                 STATUS_CANNOT_RESUME, "Requested range not satisfiable");
354
355                     case HTTP_UNAVAILABLE:
356                         parseRetryAfterHeaders(state, conn);
357                         throw new StopRequestException(
358                                 HTTP_UNAVAILABLE, conn.getResponseMessage());
359
360                     case HTTP_INTERNAL_ERROR:
361                         throw new StopRequestException(
362                                 HTTP_INTERNAL_ERROR, conn.getResponseMessage());
363
364                     default:
365                         StopRequestException.throwUnhandledHttpError(
366                                 responseCode, conn.getResponseMessage());
367                 }
368             } catch (IOException e) {
369                 // Trouble with low-level sockets
370                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
371
372             } finally {
373                 if (conn != null) conn.disconnect();
374             }
375         }
376
377         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
378     }
379
380     /**
381      * Transfer data from the given connection to the destination file.
382      */
383     private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
384         DrmManagerClient drmClient = null;
385         InputStream in = null;
386         OutputStream out = null;
387         FileDescriptor outFd = null;
388         try {
389             try {
390                 in = conn.getInputStream();
391             } catch (IOException e) {
392                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
393             }
394
395             try {
396                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
397                     drmClient = new DrmManagerClient(mContext);
398                     final RandomAccessFile file = new RandomAccessFile(
399                             new File(state.mFilename), "rw");
400                     out = new DrmOutputStream(drmClient, file, state.mMimeType);
401                     outFd = file.getFD();
402                 } else {
403                     out = new FileOutputStream(state.mFilename, true);
404                     outFd = ((FileOutputStream) out).getFD();
405                 }
406             } catch (IOException e) {
407                 throw new StopRequestException(STATUS_FILE_ERROR, e);
408             }
409
410             // Start streaming data, periodically watch for pause/cancel
411             // commands and checking disk space as needed.
412             transferData(state, in, out);
413
414             try {
415                 if (out instanceof DrmOutputStream) {
416                     ((DrmOutputStream) out).finish();
417                 }
418             } catch (IOException e) {
419                 throw new StopRequestException(STATUS_FILE_ERROR, e);
420             }
421
422         } finally {
423             if (drmClient != null) {
424                 drmClient.release();
425             }
426
427             IoUtils.closeQuietly(in);
428
429             try {
430                 if (out != null) out.flush();
431                 if (outFd != null) outFd.sync();
432             } catch (IOException e) {
433             } finally {
434                 IoUtils.closeQuietly(out);
435             }
436         }
437     }
438
439     /**
440      * Check if current connectivity is valid for this request.
441      */
442     private void checkConnectivity() throws StopRequestException {
443         // checking connectivity will apply current policy
444         mPolicyDirty = false;
445
446         final NetworkState networkUsable = mInfo.checkCanUseNetwork();
447         if (networkUsable != NetworkState.OK) {
448             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
449             if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
450                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
451                 mInfo.notifyPauseDueToSize(true);
452             } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
453                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
454                 mInfo.notifyPauseDueToSize(false);
455             }
456             throw new StopRequestException(status, networkUsable.name());
457         }
458     }
459
460     /**
461      * Transfer as much data as possible from the HTTP response to the
462      * destination file.
463      */
464     private void transferData(State state, InputStream in, OutputStream out)
465             throws StopRequestException {
466         final byte data[] = new byte[Constants.BUFFER_SIZE];
467         for (;;) {
468             int bytesRead = readFromResponse(state, data, in);
469             if (bytesRead == -1) { // success, end of stream already reached
470                 handleEndOfStream(state);
471                 return;
472             }
473
474             state.mGotData = true;
475             writeDataToDestination(state, data, bytesRead, out);
476             state.mCurrentBytes += bytesRead;
477             reportProgress(state);
478
479             if (Constants.LOGVV) {
480                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
481                       + mInfo.mUri);
482             }
483
484             checkPausedOrCanceled(state);
485         }
486     }
487
488     /**
489      * Called after a successful completion to take any necessary action on the downloaded file.
490      */
491     private void finalizeDestinationFile(State state) {
492         if (state.mFilename != null) {
493             // make sure the file is readable
494             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
495         }
496     }
497
498     /**
499      * Called just before the thread finishes, regardless of status, to take any necessary action on
500      * the downloaded file.
501      */
502     private void cleanupDestination(State state, int finalStatus) {
503         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
504             if (Constants.LOGVV) {
505                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
506             }
507             new File(state.mFilename).delete();
508             state.mFilename = null;
509         }
510     }
511
512     /**
513      * Check if the download has been paused or canceled, stopping the request appropriately if it
514      * has been.
515      */
516     private void checkPausedOrCanceled(State state) throws StopRequestException {
517         synchronized (mInfo) {
518             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
519                 throw new StopRequestException(
520                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
521             }
522             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
523                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
524             }
525         }
526
527         // if policy has been changed, trigger connectivity check
528         if (mPolicyDirty) {
529             checkConnectivity();
530         }
531     }
532
533     /**
534      * Report download progress through the database if necessary.
535      */
536     private void reportProgress(State state) {
537         final long now = SystemClock.elapsedRealtime();
538
539         final long sampleDelta = now - state.mSpeedSampleStart;
540         if (sampleDelta > 500) {
541             final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
542                     / sampleDelta;
543
544             if (state.mSpeed == 0) {
545                 state.mSpeed = sampleSpeed;
546             } else {
547                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
548             }
549
550             // Only notify once we have a full sample window
551             if (state.mSpeedSampleStart != 0) {
552                 mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed);
553             }
554
555             state.mSpeedSampleStart = now;
556             state.mSpeedSampleBytes = state.mCurrentBytes;
557         }
558
559         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
560             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
561             ContentValues values = new ContentValues();
562             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
563             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
564             state.mBytesNotified = state.mCurrentBytes;
565             state.mTimeLastNotification = now;
566         }
567     }
568
569     /**
570      * Write a data buffer to the destination file.
571      * @param data buffer containing the data to write
572      * @param bytesRead how many bytes to write from the buffer
573      */
574     private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
575             throws StopRequestException {
576         mStorageManager.verifySpaceBeforeWritingToFile(
577                 mInfo.mDestination, state.mFilename, bytesRead);
578
579         boolean forceVerified = false;
580         while (true) {
581             try {
582                 out.write(data, 0, bytesRead);
583                 return;
584             } catch (IOException ex) {
585                 // TODO: better differentiate between DRM and disk failures
586                 if (!forceVerified) {
587                     // couldn't write to file. are we out of space? check.
588                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
589                     forceVerified = true;
590                 } else {
591                     throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
592                             "Failed to write data: " + ex);
593                 }
594             }
595         }
596     }
597
598     /**
599      * Called when we've reached the end of the HTTP response stream, to update the database and
600      * check for consistency.
601      */
602     private void handleEndOfStream(State state) throws StopRequestException {
603         ContentValues values = new ContentValues();
604         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
605         if (state.mContentLength == -1) {
606             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
607         }
608         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
609
610         final boolean lengthMismatched = (state.mContentLength != -1)
611                 && (state.mCurrentBytes != state.mContentLength);
612         if (lengthMismatched) {
613             if (cannotResume(state)) {
614                 throw new StopRequestException(STATUS_CANNOT_RESUME,
615                         "mismatched content length; unable to resume");
616             } else {
617                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
618                         "closed socket before end of file");
619             }
620         }
621     }
622
623     private boolean cannotResume(State state) {
624         return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
625                 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
626     }
627
628     /**
629      * Read some data from the HTTP response stream, handling I/O errors.
630      * @param data buffer to use to read data
631      * @param entityStream stream for reading the HTTP response entity
632      * @return the number of bytes actually read or -1 if the end of the stream has been reached
633      */
634     private int readFromResponse(State state, byte[] data, InputStream entityStream)
635             throws StopRequestException {
636         try {
637             return entityStream.read(data);
638         } catch (IOException ex) {
639             // TODO: handle stream errors the same as other retries
640             if ("unexpected end of stream".equals(ex.getMessage())) {
641                 return -1;
642             }
643
644             ContentValues values = new ContentValues();
645             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
646             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
647             if (cannotResume(state)) {
648                 throw new StopRequestException(STATUS_CANNOT_RESUME,
649                         "Failed reading response: " + ex + "; unable to resume", ex);
650             } else {
651                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
652                         "Failed reading response: " + ex, ex);
653             }
654         }
655     }
656
657     /**
658      * Prepare target file based on given network response. Derives filename and
659      * target size as needed.
660      */
661     private void processResponseHeaders(State state, HttpURLConnection conn)
662             throws StopRequestException {
663         // TODO: fallocate the entire file if header gave us specific length
664
665         readResponseHeaders(state, conn);
666
667         state.mFilename = Helpers.generateSaveFile(
668                 mContext,
669                 mInfo.mUri,
670                 mInfo.mHint,
671                 state.mContentDisposition,
672                 state.mContentLocation,
673                 state.mMimeType,
674                 mInfo.mDestination,
675                 state.mContentLength,
676                 mStorageManager);
677
678         updateDatabaseFromHeaders(state);
679         // check connectivity again now that we know the total size
680         checkConnectivity();
681     }
682
683     /**
684      * Update necessary database fields based on values of HTTP response headers that have been
685      * read.
686      */
687     private void updateDatabaseFromHeaders(State state) {
688         ContentValues values = new ContentValues();
689         values.put(Downloads.Impl._DATA, state.mFilename);
690         if (state.mHeaderETag != null) {
691             values.put(Constants.ETAG, state.mHeaderETag);
692         }
693         if (state.mMimeType != null) {
694             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
695         }
696         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
697         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
698     }
699
700     /**
701      * Read headers from the HTTP response and store them into local state.
702      */
703     private void readResponseHeaders(State state, HttpURLConnection conn)
704             throws StopRequestException {
705         state.mContentDisposition = conn.getHeaderField("Content-Disposition");
706         state.mContentLocation = conn.getHeaderField("Content-Location");
707
708         if (state.mMimeType == null) {
709             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
710         }
711
712         state.mHeaderETag = conn.getHeaderField("ETag");
713
714         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
715         if (transferEncoding == null) {
716             state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
717         } else {
718             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
719             state.mContentLength = -1;
720         }
721
722         state.mTotalBytes = state.mContentLength;
723         mInfo.mTotalBytes = state.mContentLength;
724
725         final boolean noSizeInfo = state.mContentLength == -1
726                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
727         if (!mInfo.mNoIntegrity && noSizeInfo) {
728             throw new StopRequestException(STATUS_CANNOT_RESUME,
729                     "can't know size of download, giving up");
730         }
731     }
732
733     private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
734         state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
735         if (state.mRetryAfter < 0) {
736             state.mRetryAfter = 0;
737         } else {
738             if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
739                 state.mRetryAfter = Constants.MIN_RETRY_AFTER;
740             } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
741                 state.mRetryAfter = Constants.MAX_RETRY_AFTER;
742             }
743             state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
744             state.mRetryAfter *= 1000;
745         }
746     }
747
748     /**
749      * Prepare the destination file to receive data.  If the file already exists, we'll set up
750      * appropriately for resumption.
751      */
752     private void setupDestinationFile(State state) throws StopRequestException {
753         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
754             if (Constants.LOGV) {
755                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
756                         ", and state.mFilename: " + state.mFilename);
757             }
758             if (!Helpers.isFilenameValid(state.mFilename,
759                     mStorageManager.getDownloadDataDirectory())) {
760                 // this should never happen
761                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
762                         "found invalid internal destination filename");
763             }
764             // We're resuming a download that got interrupted
765             File f = new File(state.mFilename);
766             if (f.exists()) {
767                 if (Constants.LOGV) {
768                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
769                             ", and state.mFilename: " + state.mFilename);
770                 }
771                 long fileLength = f.length();
772                 if (fileLength == 0) {
773                     // The download hadn't actually started, we can restart from scratch
774                     if (Constants.LOGVV) {
775                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
776                                 + state.mFilename);
777                     }
778                     f.delete();
779                     state.mFilename = null;
780                     if (Constants.LOGV) {
781                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
782                                 ", BUT starting from scratch again: ");
783                     }
784                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
785                     // This should've been caught upon failure
786                     if (Constants.LOGVV) {
787                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
788                                 + state.mFilename);
789                     }
790                     f.delete();
791                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
792                             "Trying to resume a download that can't be resumed");
793                 } else {
794                     // All right, we'll be able to resume this download
795                     if (Constants.LOGV) {
796                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
797                                 ", and starting with file of length: " + fileLength);
798                     }
799                     state.mCurrentBytes = (int) fileLength;
800                     if (mInfo.mTotalBytes != -1) {
801                         state.mContentLength = mInfo.mTotalBytes;
802                     }
803                     state.mHeaderETag = mInfo.mETag;
804                     state.mContinuingDownload = true;
805                     if (Constants.LOGV) {
806                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
807                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
808                                 ", and setting mContinuingDownload to true: ");
809                     }
810                 }
811             }
812         }
813     }
814
815     /**
816      * Add custom headers for this download to the HTTP request.
817      */
818     private void addRequestHeaders(State state, HttpURLConnection conn) {
819         for (Pair<String, String> header : mInfo.getHeaders()) {
820             conn.addRequestProperty(header.first, header.second);
821         }
822
823         // Only splice in user agent when not already defined
824         if (conn.getRequestProperty("User-Agent") == null) {
825             conn.addRequestProperty("User-Agent", userAgent());
826         }
827
828         // Defeat transparent gzip compression, since it doesn't allow us to
829         // easily resume partial downloads.
830         conn.setRequestProperty("Accept-Encoding", "identity");
831
832         if (state.mContinuingDownload) {
833             if (state.mHeaderETag != null) {
834                 conn.addRequestProperty("If-Match", state.mHeaderETag);
835             }
836             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
837         }
838     }
839
840     /**
841      * Stores information about the completed download, and notifies the initiating application.
842      */
843     private void notifyDownloadCompleted(
844             State state, int finalStatus, String errorMsg, int numFailed) {
845         notifyThroughDatabase(state, finalStatus, errorMsg, numFailed);
846         if (Downloads.Impl.isStatusCompleted(finalStatus)) {
847             mInfo.sendIntentIfRequested();
848         }
849     }
850
851     private void notifyThroughDatabase(
852             State state, int finalStatus, String errorMsg, int numFailed) {
853         ContentValues values = new ContentValues();
854         values.put(Downloads.Impl.COLUMN_STATUS, finalStatus);
855         values.put(Downloads.Impl._DATA, state.mFilename);
856         values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
857         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
858         values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed);
859         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter);
860
861         if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) {
862             values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri);
863         }
864
865         // save the error message. could be useful to developers.
866         if (!TextUtils.isEmpty(errorMsg)) {
867             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
868         }
869         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
870     }
871
872     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
873         @Override
874         public void onUidRulesChanged(int uid, int uidRules) {
875             // caller is NPMS, since we only register with them
876             if (uid == mInfo.mUid) {
877                 mPolicyDirty = true;
878             }
879         }
880
881         @Override
882         public void onMeteredIfacesChanged(String[] meteredIfaces) {
883             // caller is NPMS, since we only register with them
884             mPolicyDirty = true;
885         }
886
887         @Override
888         public void onRestrictBackgroundChanged(boolean restrictBackground) {
889             // caller is NPMS, since we only register with them
890             mPolicyDirty = true;
891         }
892     };
893
894     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
895         try {
896             return Long.parseLong(conn.getHeaderField(field));
897         } catch (NumberFormatException e) {
898             return defaultValue;
899         }
900     }
901
902     /**
903      * Return if given status is eligible to be treated as
904      * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
905      */
906     public static boolean isStatusRetryable(int status) {
907         switch (status) {
908             case STATUS_HTTP_DATA_ERROR:
909             case HTTP_UNAVAILABLE:
910             case HTTP_INTERNAL_ERROR:
911                 return true;
912             default:
913                 return false;
914         }
915     }
916 }