am cc167f03: (-s ours) am bb611587: (-s ours) Import translations. DO NOT MERGE
[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_UNHANDLED_HTTP_CODE;
26 import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR;
27 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
28 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
29 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
30 import static com.android.providers.downloads.Constants.TAG;
31 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
32 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
33 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
34 import static java.net.HttpURLConnection.HTTP_OK;
35 import static java.net.HttpURLConnection.HTTP_PARTIAL;
36 import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
37 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
38 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
39
40 import android.content.ContentValues;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.drm.DrmManagerClient;
44 import android.drm.DrmOutputStream;
45 import android.net.ConnectivityManager;
46 import android.net.INetworkPolicyListener;
47 import android.net.NetworkInfo;
48 import android.net.NetworkPolicyManager;
49 import android.net.TrafficStats;
50 import android.os.ParcelFileDescriptor;
51 import android.os.PowerManager;
52 import android.os.Process;
53 import android.os.SystemClock;
54 import android.os.WorkSource;
55 import android.provider.Downloads;
56 import android.system.ErrnoException;
57 import android.system.Os;
58 import android.system.OsConstants;
59 import android.util.Log;
60 import android.util.Pair;
61
62 import com.android.providers.downloads.DownloadInfo.NetworkState;
63
64 import libcore.io.IoUtils;
65
66 import java.io.File;
67 import java.io.FileDescriptor;
68 import java.io.FileNotFoundException;
69 import java.io.IOException;
70 import java.io.InputStream;
71 import java.io.OutputStream;
72 import java.net.HttpURLConnection;
73 import java.net.MalformedURLException;
74 import java.net.ProtocolException;
75 import java.net.URL;
76 import java.net.URLConnection;
77
78 /**
79  * Task which executes a given {@link DownloadInfo}: making network requests,
80  * persisting data to disk, and updating {@link DownloadProvider}.
81  * <p>
82  * To know if a download is successful, we need to know either the final content
83  * length to expect, or the transfer to be chunked. To resume an interrupted
84  * download, we need an ETag.
85  * <p>
86  * Failed network requests are retried several times before giving up. Local
87  * disk errors fail immediately and are not retried.
88  */
89 public class DownloadThread implements Runnable {
90
91     // TODO: bind each download to a specific network interface to avoid state
92     // checking races once we have ConnectivityManager API
93
94     // TODO: add support for saving to content://
95
96     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
97     private static final int HTTP_TEMP_REDIRECT = 307;
98
99     private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
100
101     private final Context mContext;
102     private final SystemFacade mSystemFacade;
103     private final DownloadNotifier mNotifier;
104
105     private final long mId;
106
107     /**
108      * Info object that should be treated as read-only. Any potentially mutated
109      * fields are tracked in {@link #mInfoDelta}. If a field exists in
110      * {@link #mInfoDelta}, it must not be read from {@link #mInfo}.
111      */
112     private final DownloadInfo mInfo;
113     private final DownloadInfoDelta mInfoDelta;
114
115     private volatile boolean mPolicyDirty;
116
117     /**
118      * Local changes to {@link DownloadInfo}. These are kept local to avoid
119      * racing with the thread that updates based on change notifications.
120      */
121     private class DownloadInfoDelta {
122         public String mUri;
123         public String mFileName;
124         public String mMimeType;
125         public int mStatus;
126         public int mNumFailed;
127         public int mRetryAfter;
128         public long mTotalBytes;
129         public long mCurrentBytes;
130         public String mETag;
131
132         public String mErrorMsg;
133
134         public DownloadInfoDelta(DownloadInfo info) {
135             mUri = info.mUri;
136             mFileName = info.mFileName;
137             mMimeType = info.mMimeType;
138             mStatus = info.mStatus;
139             mNumFailed = info.mNumFailed;
140             mRetryAfter = info.mRetryAfter;
141             mTotalBytes = info.mTotalBytes;
142             mCurrentBytes = info.mCurrentBytes;
143             mETag = info.mETag;
144         }
145
146         /**
147          * Push update of current delta values to provider.
148          */
149         public void writeToDatabase() {
150             final ContentValues values = new ContentValues();
151
152             values.put(Downloads.Impl.COLUMN_URI, mUri);
153             values.put(Downloads.Impl._DATA, mFileName);
154             values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
155             values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
156             values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
157             values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
158             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
159             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
160             values.put(Constants.ETAG, mETag);
161
162             values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
163             values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);
164
165             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
166         }
167     }
168
169     /**
170      * Flag indicating if we've made forward progress transferring file data
171      * from a remote server.
172      */
173     private boolean mMadeProgress = false;
174
175     /**
176      * Details from the last time we pushed a database update.
177      */
178     private long mLastUpdateBytes = 0;
179     private long mLastUpdateTime = 0;
180
181     private int mNetworkType = ConnectivityManager.TYPE_NONE;
182
183     /** Historical bytes/second speed of this download. */
184     private long mSpeed;
185     /** Time when current sample started. */
186     private long mSpeedSampleStart;
187     /** Bytes transferred since current sample started. */
188     private long mSpeedSampleBytes;
189
190     public DownloadThread(Context context, SystemFacade systemFacade, DownloadNotifier notifier,
191             DownloadInfo info) {
192         mContext = context;
193         mSystemFacade = systemFacade;
194         mNotifier = notifier;
195
196         mId = info.mId;
197         mInfo = info;
198         mInfoDelta = new DownloadInfoDelta(info);
199     }
200
201     @Override
202     public void run() {
203         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
204
205         // Skip when download already marked as finished; this download was
206         // probably started again while racing with UpdateThread.
207         if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId)
208                 == Downloads.Impl.STATUS_SUCCESS) {
209             logDebug("Already finished; skipping");
210             return;
211         }
212
213         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
214         PowerManager.WakeLock wakeLock = null;
215         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
216
217         try {
218             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
219             wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
220             wakeLock.acquire();
221
222             // while performing download, register for rules updates
223             netPolicy.registerListener(mPolicyListener);
224
225             logDebug("Starting");
226
227             // Remember which network this download started on; used to
228             // determine if errors were due to network changes.
229             final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
230             if (info != null) {
231                 mNetworkType = info.getType();
232             }
233
234             // Network traffic on this thread should be counted against the
235             // requesting UID, and is tagged with well-known value.
236             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
237             TrafficStats.setThreadStatsUid(mInfo.mUid);
238
239             executeDownload();
240
241             mInfoDelta.mStatus = STATUS_SUCCESS;
242             TrafficStats.incrementOperationCount(1);
243
244             // If we just finished a chunked file, record total size
245             if (mInfoDelta.mTotalBytes == -1) {
246                 mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes;
247             }
248
249         } catch (StopRequestException e) {
250             mInfoDelta.mStatus = e.getFinalStatus();
251             mInfoDelta.mErrorMsg = e.getMessage();
252
253             logWarning("Stop requested with status "
254                     + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
255                     + mInfoDelta.mErrorMsg);
256
257             // Nobody below our level should request retries, since we handle
258             // failure counts at this level.
259             if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
260                 throw new IllegalStateException("Execution should always throw final error codes");
261             }
262
263             // Some errors should be retryable, unless we fail too many times.
264             if (isStatusRetryable(mInfoDelta.mStatus)) {
265                 if (mMadeProgress) {
266                     mInfoDelta.mNumFailed = 1;
267                 } else {
268                     mInfoDelta.mNumFailed += 1;
269                 }
270
271                 if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
272                     final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
273                     if (info != null && info.getType() == mNetworkType && info.isConnected()) {
274                         // Underlying network is still intact, use normal backoff
275                         mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
276                     } else {
277                         // Network changed, retry on any next available
278                         mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
279                     }
280
281                     if ((mInfoDelta.mETag == null && mMadeProgress)
282                             || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
283                         // However, if we wrote data and have no ETag to verify
284                         // contents against later, we can't actually resume.
285                         mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
286                     }
287                 }
288             }
289
290         } catch (Throwable t) {
291             mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
292             mInfoDelta.mErrorMsg = t.toString();
293
294             logError("Failed: " + mInfoDelta.mErrorMsg, t);
295
296         } finally {
297             logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus));
298
299             mNotifier.notifyDownloadSpeed(mId, 0);
300
301             finalizeDestination();
302
303             mInfoDelta.writeToDatabase();
304
305             if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
306                 mInfo.sendIntentIfRequested();
307             }
308
309             TrafficStats.clearThreadStatsTag();
310             TrafficStats.clearThreadStatsUid();
311
312             netPolicy.unregisterListener(mPolicyListener);
313
314             if (wakeLock != null) {
315                 wakeLock.release();
316                 wakeLock = null;
317             }
318         }
319     }
320
321     /**
322      * Fully execute a single download request. Setup and send the request,
323      * handle the response, and transfer the data to the destination file.
324      */
325     private void executeDownload() throws StopRequestException {
326         final boolean resuming = mInfoDelta.mCurrentBytes != 0;
327
328         URL url;
329         try {
330             // TODO: migrate URL sanity checking into client side of API
331             url = new URL(mInfoDelta.mUri);
332         } catch (MalformedURLException e) {
333             throw new StopRequestException(STATUS_BAD_REQUEST, e);
334         }
335
336         int redirectionCount = 0;
337         while (redirectionCount++ < Constants.MAX_REDIRECTS) {
338             // Open connection and follow any redirects until we have a useful
339             // response with body.
340             HttpURLConnection conn = null;
341             try {
342                 checkConnectivity();
343                 conn = (HttpURLConnection) url.openConnection();
344                 conn.setInstanceFollowRedirects(false);
345                 conn.setConnectTimeout(DEFAULT_TIMEOUT);
346                 conn.setReadTimeout(DEFAULT_TIMEOUT);
347
348                 addRequestHeaders(conn, resuming);
349
350                 final int responseCode = conn.getResponseCode();
351                 switch (responseCode) {
352                     case HTTP_OK:
353                         if (resuming) {
354                             throw new StopRequestException(
355                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
356                         }
357                         parseOkHeaders(conn);
358                         transferData(conn);
359                         return;
360
361                     case HTTP_PARTIAL:
362                         if (!resuming) {
363                             throw new StopRequestException(
364                                     STATUS_CANNOT_RESUME, "Expected OK, but received partial");
365                         }
366                         transferData(conn);
367                         return;
368
369                     case HTTP_MOVED_PERM:
370                     case HTTP_MOVED_TEMP:
371                     case HTTP_SEE_OTHER:
372                     case HTTP_TEMP_REDIRECT:
373                         final String location = conn.getHeaderField("Location");
374                         url = new URL(url, location);
375                         if (responseCode == HTTP_MOVED_PERM) {
376                             // Push updated URL back to database
377                             mInfoDelta.mUri = url.toString();
378                         }
379                         continue;
380
381                     case HTTP_PRECON_FAILED:
382                         throw new StopRequestException(
383                                 STATUS_CANNOT_RESUME, "Precondition failed");
384
385                     case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
386                         throw new StopRequestException(
387                                 STATUS_CANNOT_RESUME, "Requested range not satisfiable");
388
389                     case HTTP_UNAVAILABLE:
390                         parseUnavailableHeaders(conn);
391                         throw new StopRequestException(
392                                 HTTP_UNAVAILABLE, conn.getResponseMessage());
393
394                     case HTTP_INTERNAL_ERROR:
395                         throw new StopRequestException(
396                                 HTTP_INTERNAL_ERROR, conn.getResponseMessage());
397
398                     default:
399                         StopRequestException.throwUnhandledHttpError(
400                                 responseCode, conn.getResponseMessage());
401                 }
402
403             } catch (IOException e) {
404                 if (e instanceof ProtocolException
405                         && e.getMessage().startsWith("Unexpected status line")) {
406                     throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e);
407                 } else {
408                     // Trouble with low-level sockets
409                     throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
410                 }
411
412             } finally {
413                 if (conn != null) conn.disconnect();
414             }
415         }
416
417         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
418     }
419
420     /**
421      * Transfer data from the given connection to the destination file.
422      */
423     private void transferData(HttpURLConnection conn) throws StopRequestException {
424
425         // To detect when we're really finished, we either need a length or
426         // chunked encoding.
427         final boolean hasLength = mInfoDelta.mTotalBytes != -1;
428         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
429         final boolean isChunked = "chunked".equalsIgnoreCase(transferEncoding);
430         if (!hasLength && !isChunked) {
431             throw new StopRequestException(
432                     STATUS_CANNOT_RESUME, "can't know size of download, giving up");
433         }
434
435         DrmManagerClient drmClient = null;
436         ParcelFileDescriptor outPfd = null;
437         FileDescriptor outFd = null;
438         InputStream in = null;
439         OutputStream out = null;
440         try {
441             try {
442                 in = conn.getInputStream();
443             } catch (IOException e) {
444                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
445             }
446
447             try {
448                 outPfd = mContext.getContentResolver()
449                         .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
450                 outFd = outPfd.getFileDescriptor();
451
452                 if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
453                     drmClient = new DrmManagerClient(mContext);
454                     out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);
455                 } else {
456                     out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
457                 }
458
459                 // Pre-flight disk space requirements, when known
460                 if (mInfoDelta.mTotalBytes > 0) {
461                     final long curSize = Os.fstat(outFd).st_size;
462                     final long newBytes = mInfoDelta.mTotalBytes - curSize;
463
464                     StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
465
466                     try {
467                         // We found enough space, so claim it for ourselves
468                         Os.posix_fallocate(outFd, 0, mInfoDelta.mTotalBytes);
469                     } catch (ErrnoException e) {
470                         if (e.errno == OsConstants.ENOTSUP) {
471                             Log.w(TAG, "fallocate() said ENOTSUP; falling back to ftruncate()");
472                             Os.ftruncate(outFd, mInfoDelta.mTotalBytes);
473                         } else {
474                             throw e;
475                         }
476                     }
477                 }
478
479                 // Move into place to begin writing
480                 Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET);
481
482             } catch (ErrnoException e) {
483                 throw new StopRequestException(STATUS_FILE_ERROR, e);
484             } catch (IOException e) {
485                 throw new StopRequestException(STATUS_FILE_ERROR, e);
486             }
487
488             // Start streaming data, periodically watch for pause/cancel
489             // commands and checking disk space as needed.
490             transferData(in, out, outFd);
491
492             try {
493                 if (out instanceof DrmOutputStream) {
494                     ((DrmOutputStream) out).finish();
495                 }
496             } catch (IOException e) {
497                 throw new StopRequestException(STATUS_FILE_ERROR, e);
498             }
499
500         } finally {
501             if (drmClient != null) {
502                 drmClient.release();
503             }
504
505             IoUtils.closeQuietly(in);
506
507             try {
508                 if (out != null) out.flush();
509                 if (outFd != null) outFd.sync();
510             } catch (IOException e) {
511             } finally {
512                 IoUtils.closeQuietly(out);
513             }
514         }
515     }
516
517     /**
518      * Transfer as much data as possible from the HTTP response to the
519      * destination file.
520      */
521     private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
522             throws StopRequestException {
523         final byte buffer[] = new byte[Constants.BUFFER_SIZE];
524         while (true) {
525             checkPausedOrCanceled();
526
527             int len = -1;
528             try {
529                 len = in.read(buffer);
530             } catch (IOException e) {
531                 throw new StopRequestException(
532                         STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
533             }
534
535             if (len == -1) {
536                 break;
537             }
538
539             try {
540                 // When streaming, ensure space before each write
541                 if (mInfoDelta.mTotalBytes == -1) {
542                     final long curSize = Os.fstat(outFd).st_size;
543                     final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize;
544
545                     StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
546                 }
547
548                 out.write(buffer, 0, len);
549
550                 mMadeProgress = true;
551                 mInfoDelta.mCurrentBytes += len;
552
553                 updateProgress(outFd);
554
555             } catch (ErrnoException e) {
556                 throw new StopRequestException(STATUS_FILE_ERROR, e);
557             } catch (IOException e) {
558                 throw new StopRequestException(STATUS_FILE_ERROR, e);
559             }
560         }
561
562         // Finished without error; verify length if known
563         if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) {
564             throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch");
565         }
566     }
567
568     /**
569      * Called just before the thread finishes, regardless of status, to take any
570      * necessary action on the downloaded file.
571      */
572     private void finalizeDestination() {
573         if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) {
574             // When error, free up any disk space
575             try {
576                 final ParcelFileDescriptor target = mContext.getContentResolver()
577                         .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
578                 try {
579                     Os.ftruncate(target.getFileDescriptor(), 0);
580                 } catch (ErrnoException ignored) {
581                 } finally {
582                     IoUtils.closeQuietly(target);
583                 }
584             } catch (FileNotFoundException ignored) {
585             }
586
587             // Delete if local file
588             if (mInfoDelta.mFileName != null) {
589                 new File(mInfoDelta.mFileName).delete();
590                 mInfoDelta.mFileName = null;
591             }
592
593         } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
594             // When success, open access if local file
595             if (mInfoDelta.mFileName != null) {
596                 try {
597                     // TODO: remove this once PackageInstaller works with content://
598                     Os.chmod(mInfoDelta.mFileName, 0644);
599                 } catch (ErrnoException ignored) {
600                 }
601
602                 if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
603                     try {
604                         // Move into final resting place, if needed
605                         final File before = new File(mInfoDelta.mFileName);
606                         final File beforeDir = Helpers.getRunningDestinationDirectory(
607                                 mContext, mInfo.mDestination);
608                         final File afterDir = Helpers.getSuccessDestinationDirectory(
609                                 mContext, mInfo.mDestination);
610                         if (!beforeDir.equals(afterDir)
611                                 && before.getParentFile().equals(beforeDir)) {
612                             final File after = new File(afterDir, before.getName());
613                             if (before.renameTo(after)) {
614                                 mInfoDelta.mFileName = after.getAbsolutePath();
615                             }
616                         }
617                     } catch (IOException ignored) {
618                     }
619                 }
620             }
621         }
622     }
623
624     /**
625      * Check if current connectivity is valid for this request.
626      */
627     private void checkConnectivity() throws StopRequestException {
628         // checking connectivity will apply current policy
629         mPolicyDirty = false;
630
631         final NetworkState networkUsable = mInfo.checkCanUseNetwork(mInfoDelta.mTotalBytes);
632         if (networkUsable != NetworkState.OK) {
633             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
634             if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
635                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
636                 mInfo.notifyPauseDueToSize(true);
637             } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
638                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
639                 mInfo.notifyPauseDueToSize(false);
640             }
641             throw new StopRequestException(status, networkUsable.name());
642         }
643     }
644
645     /**
646      * Check if the download has been paused or canceled, stopping the request
647      * appropriately if it has been.
648      */
649     private void checkPausedOrCanceled() throws StopRequestException {
650         synchronized (mInfo) {
651             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
652                 throw new StopRequestException(
653                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
654             }
655             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) {
656                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
657             }
658         }
659
660         // if policy has been changed, trigger connectivity check
661         if (mPolicyDirty) {
662             checkConnectivity();
663         }
664     }
665
666     /**
667      * Report download progress through the database if necessary.
668      */
669     private void updateProgress(FileDescriptor outFd) throws IOException {
670         final long now = SystemClock.elapsedRealtime();
671         final long currentBytes = mInfoDelta.mCurrentBytes;
672
673         final long sampleDelta = now - mSpeedSampleStart;
674         if (sampleDelta > 500) {
675             final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000)
676                     / sampleDelta;
677
678             if (mSpeed == 0) {
679                 mSpeed = sampleSpeed;
680             } else {
681                 mSpeed = ((mSpeed * 3) + sampleSpeed) / 4;
682             }
683
684             // Only notify once we have a full sample window
685             if (mSpeedSampleStart != 0) {
686                 mNotifier.notifyDownloadSpeed(mId, mSpeed);
687             }
688
689             mSpeedSampleStart = now;
690             mSpeedSampleBytes = currentBytes;
691         }
692
693         final long bytesDelta = currentBytes - mLastUpdateBytes;
694         final long timeDelta = now - mLastUpdateTime;
695         if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
696             // fsync() to ensure that current progress has been flushed to disk,
697             // so we can always resume based on latest database information.
698             outFd.sync();
699
700             mInfoDelta.writeToDatabase();
701
702             mLastUpdateBytes = currentBytes;
703             mLastUpdateTime = now;
704         }
705     }
706
707     /**
708      * Process response headers from first server response. This derives its
709      * filename, size, and ETag.
710      */
711     private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException {
712         if (mInfoDelta.mFileName == null) {
713             final String contentDisposition = conn.getHeaderField("Content-Disposition");
714             final String contentLocation = conn.getHeaderField("Content-Location");
715
716             try {
717                 mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri,
718                         mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType,
719                         mInfo.mDestination);
720             } catch (IOException e) {
721                 throw new StopRequestException(
722                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e);
723             }
724         }
725
726         if (mInfoDelta.mMimeType == null) {
727             mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
728         }
729
730         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
731         if (transferEncoding == null) {
732             mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1);
733         } else {
734             mInfoDelta.mTotalBytes = -1;
735         }
736
737         mInfoDelta.mETag = conn.getHeaderField("ETag");
738
739         mInfoDelta.writeToDatabase();
740
741         // Check connectivity again now that we know the total size
742         checkConnectivity();
743     }
744
745     private void parseUnavailableHeaders(HttpURLConnection conn) {
746         long retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
747         if (retryAfter < 0) {
748             retryAfter = 0;
749         } else {
750             if (retryAfter < Constants.MIN_RETRY_AFTER) {
751                 retryAfter = Constants.MIN_RETRY_AFTER;
752             } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
753                 retryAfter = Constants.MAX_RETRY_AFTER;
754             }
755             retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
756         }
757
758         mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS);
759     }
760
761     /**
762      * Add custom headers for this download to the HTTP request.
763      */
764     private void addRequestHeaders(HttpURLConnection conn, boolean resuming) {
765         for (Pair<String, String> header : mInfo.getHeaders()) {
766             conn.addRequestProperty(header.first, header.second);
767         }
768
769         // Only splice in user agent when not already defined
770         if (conn.getRequestProperty("User-Agent") == null) {
771             conn.addRequestProperty("User-Agent", mInfo.getUserAgent());
772         }
773
774         // Defeat transparent gzip compression, since it doesn't allow us to
775         // easily resume partial downloads.
776         conn.setRequestProperty("Accept-Encoding", "identity");
777
778         if (resuming) {
779             if (mInfoDelta.mETag != null) {
780                 conn.addRequestProperty("If-Match", mInfoDelta.mETag);
781             }
782             conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-");
783         }
784     }
785
786     private void logDebug(String msg) {
787         Log.d(TAG, "[" + mId + "] " + msg);
788     }
789
790     private void logWarning(String msg) {
791         Log.w(TAG, "[" + mId + "] " + msg);
792     }
793
794     private void logError(String msg, Throwable t) {
795         Log.e(TAG, "[" + mId + "] " + msg, t);
796     }
797
798     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
799         @Override
800         public void onUidRulesChanged(int uid, int uidRules) {
801             // caller is NPMS, since we only register with them
802             if (uid == mInfo.mUid) {
803                 mPolicyDirty = true;
804             }
805         }
806
807         @Override
808         public void onMeteredIfacesChanged(String[] meteredIfaces) {
809             // caller is NPMS, since we only register with them
810             mPolicyDirty = true;
811         }
812
813         @Override
814         public void onRestrictBackgroundChanged(boolean restrictBackground) {
815             // caller is NPMS, since we only register with them
816             mPolicyDirty = true;
817         }
818     };
819
820     private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
821         try {
822             return Long.parseLong(conn.getHeaderField(field));
823         } catch (NumberFormatException e) {
824             return defaultValue;
825         }
826     }
827
828     /**
829      * Return if given status is eligible to be treated as
830      * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
831      */
832     public static boolean isStatusRetryable(int status) {
833         switch (status) {
834             case STATUS_HTTP_DATA_ERROR:
835             case HTTP_UNAVAILABLE:
836             case HTTP_INTERNAL_ERROR:
837                 return true;
838             default:
839                 return false;
840         }
841     }
842 }