NPE check before finalizeDestinationFile().
[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.Manifest.permission.MANAGE_NETWORK_POLICY;
20
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.net.INetworkPolicyListener;
24 import android.net.NetworkPolicyManager;
25 import android.net.Proxy;
26 import android.net.TrafficStats;
27 import android.net.http.AndroidHttpClient;
28 import android.os.FileUtils;
29 import android.os.PowerManager;
30 import android.os.Process;
31 import android.provider.Downloads;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.util.Pair;
35
36 import org.apache.http.Header;
37 import org.apache.http.HttpResponse;
38 import org.apache.http.client.methods.HttpGet;
39 import org.apache.http.conn.params.ConnRouteParams;
40
41 import java.io.File;
42 import java.io.FileNotFoundException;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.SyncFailedException;
47 import java.net.URI;
48 import java.net.URISyntaxException;
49 import java.util.Locale;
50
51 /**
52  * Runs an actual download
53  */
54 public class DownloadThread extends Thread {
55
56     private final Context mContext;
57     private final DownloadInfo mInfo;
58     private final SystemFacade mSystemFacade;
59     private final StorageManager mStorageManager;
60     private DrmConvertSession mDrmConvertSession;
61
62     private volatile boolean mPolicyDirty;
63
64     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
65             StorageManager storageManager) {
66         mContext = context;
67         mSystemFacade = systemFacade;
68         mInfo = info;
69         mStorageManager = storageManager;
70     }
71
72     /**
73      * Returns the user agent provided by the initiating app, or use the default one
74      */
75     private String userAgent() {
76         String userAgent = mInfo.mUserAgent;
77         if (userAgent != null) {
78         }
79         if (userAgent == null) {
80             userAgent = Constants.DEFAULT_USER_AGENT;
81         }
82         return userAgent;
83     }
84
85     /**
86      * State for the entire run() method.
87      */
88     static class State {
89         public String mFilename;
90         public FileOutputStream mStream;
91         public String mMimeType;
92         public boolean mCountRetry = false;
93         public int mRetryAfter = 0;
94         public int mRedirectCount = 0;
95         public String mNewUri;
96         public boolean mGotData = false;
97         public String mRequestUri;
98         public long mTotalBytes = -1;
99         public long mCurrentBytes = 0;
100         public String mHeaderETag;
101         public boolean mContinuingDownload = false;
102         public long mBytesNotified = 0;
103         public long mTimeLastNotification = 0;
104
105         public State(DownloadInfo info) {
106             mMimeType = sanitizeMimeType(info.mMimeType);
107             mRequestUri = info.mUri;
108             mFilename = info.mFileName;
109             mTotalBytes = info.mTotalBytes;
110             mCurrentBytes = info.mCurrentBytes;
111         }
112     }
113
114     /**
115      * State within executeDownload()
116      */
117     private static class InnerState {
118         public String mHeaderContentLength;
119         public String mHeaderContentDisposition;
120         public String mHeaderContentLocation;
121     }
122
123     /**
124      * Raised from methods called by executeDownload() to indicate that the download should be
125      * retried immediately.
126      */
127     private class RetryDownload extends Throwable {}
128
129     /**
130      * Executes the download in a separate thread
131      */
132     @Override
133     public void run() {
134         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
135
136         State state = new State(mInfo);
137         AndroidHttpClient client = null;
138         PowerManager.WakeLock wakeLock = null;
139         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
140         String errorMsg = null;
141
142         final NetworkPolicyManager netPolicy = NetworkPolicyManager.getSystemService(mContext);
143         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
144
145         try {
146             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
147             wakeLock.acquire();
148
149             // while performing download, register for rules updates
150             netPolicy.registerListener(mPolicyListener);
151
152             if (Constants.LOGV) {
153                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
154             }
155
156             client = AndroidHttpClient.newInstance(userAgent(), mContext);
157
158             // network traffic on this thread should be counted against the
159             // requesting uid, and is tagged with well-known value.
160             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
161             TrafficStats.setThreadStatsUid(mInfo.mUid);
162
163             boolean finished = false;
164             while(!finished) {
165                 if (state.mCurrentBytes == state.mTotalBytes) {
166                     Log.i(Constants.TAG, "Skipping initiating request for download " +
167                           mInfo.mId + "; already completed");
168                     finished = true;
169                     break;
170                 }
171
172                 Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
173                 // Set or unset proxy, which may have changed since last GET request.
174                 // setDefaultProxy() supports null as proxy parameter.
175                 ConnRouteParams.setDefaultProxy(client.getParams(),
176                         Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
177                 HttpGet request = new HttpGet(state.mRequestUri);
178                 try {
179                     executeDownload(state, client, request);
180                     finished = true;
181                 } catch (RetryDownload exc) {
182                     // fall through
183                 } finally {
184                     request.abort();
185                     request = null;
186                 }
187             }
188
189             if (Constants.LOGV) {
190                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
191             }
192             finalizeDestinationFile(state);
193             finalStatus = Downloads.Impl.STATUS_SUCCESS;
194         } catch (StopRequestException error) {
195             // remove the cause before printing, in case it contains PII
196             errorMsg = error.getMessage();
197             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
198             Log.w(Constants.TAG, msg);
199             if (Constants.LOGV) {
200                 Log.w(Constants.TAG, msg, error);
201             }
202             finalStatus = error.mFinalStatus;
203             // fall through to finally block
204         } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
205             errorMsg = ex.getMessage();
206             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
207             Log.w(Constants.TAG, msg, ex);
208             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
209             // falls through to the code that reports an error
210         } finally {
211             TrafficStats.clearThreadStatsTag();
212             TrafficStats.clearThreadStatsUid();
213
214             if (client != null) {
215                 client.close();
216                 client = null;
217             }
218             cleanupDestination(state, finalStatus);
219             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
220                                     state.mGotData, state.mFilename,
221                                     state.mNewUri, state.mMimeType, errorMsg);
222             DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
223
224             netPolicy.unregisterListener(mPolicyListener);
225
226             if (wakeLock != null) {
227                 wakeLock.release();
228                 wakeLock = null;
229             }
230         }
231         mStorageManager.incrementNumDownloadsSoFar();
232     }
233
234     /**
235      * Fully execute a single download request - setup and send the request, handle the response,
236      * and transfer the data to the destination file.
237      */
238     private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
239             throws StopRequestException, RetryDownload {
240         InnerState innerState = new InnerState();
241         byte data[] = new byte[Constants.BUFFER_SIZE];
242
243         setupDestinationFile(state, innerState);
244         addRequestHeaders(state, request);
245
246         // check just before sending the request to avoid using an invalid connection at all
247         checkConnectivity();
248
249         HttpResponse response = sendRequest(state, client, request);
250         handleExceptionalStatus(state, innerState, response);
251
252         if (Constants.LOGV) {
253             Log.v(Constants.TAG, "received response for " + mInfo.mUri);
254         }
255
256         processResponseHeaders(state, innerState, response);
257         InputStream entityStream = openResponseEntity(state, response);
258         transferData(state, innerState, data, entityStream);
259     }
260
261     /**
262      * Check if current connectivity is valid for this request.
263      */
264     private void checkConnectivity() throws StopRequestException {
265         // checking connectivity will apply current policy
266         mPolicyDirty = false;
267
268         int networkUsable = mInfo.checkCanUseNetwork();
269         if (networkUsable != DownloadInfo.NETWORK_OK) {
270             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
271             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
272                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
273                 mInfo.notifyPauseDueToSize(true);
274             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
275                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
276                 mInfo.notifyPauseDueToSize(false);
277             } else if (networkUsable == DownloadInfo.NETWORK_BLOCKED) {
278                 status = Downloads.Impl.STATUS_BLOCKED;
279             }
280             throw new StopRequestException(status,
281                     mInfo.getLogMessageForNetworkError(networkUsable));
282         }
283     }
284
285     /**
286      * Transfer as much data as possible from the HTTP response to the destination file.
287      * @param data buffer to use to read data
288      * @param entityStream stream for reading the HTTP response entity
289      */
290     private void transferData(
291             State state, InnerState innerState, byte[] data, InputStream entityStream)
292             throws StopRequestException {
293         for (;;) {
294             int bytesRead = readFromResponse(state, innerState, data, entityStream);
295             if (bytesRead == -1) { // success, end of stream already reached
296                 handleEndOfStream(state, innerState);
297                 return;
298             }
299
300             state.mGotData = true;
301             writeDataToDestination(state, data, bytesRead);
302             state.mCurrentBytes += bytesRead;
303             reportProgress(state, innerState);
304
305             if (Constants.LOGVV) {
306                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
307                       + mInfo.mUri);
308             }
309
310             checkPausedOrCanceled(state);
311         }
312     }
313
314     /**
315      * Called after a successful completion to take any necessary action on the downloaded file.
316      */
317     private void finalizeDestinationFile(State state) throws StopRequestException {
318         if (state.mFilename != null) {
319             // make sure the file is readable
320             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
321             syncDestination(state);
322         }
323     }
324
325     /**
326      * Called just before the thread finishes, regardless of status, to take any necessary action on
327      * the downloaded file.
328      */
329     private void cleanupDestination(State state, int finalStatus) {
330         if (mDrmConvertSession != null) {
331             finalStatus = mDrmConvertSession.close(state.mFilename);
332         }
333
334         closeDestination(state);
335         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
336             new File(state.mFilename).delete();
337             state.mFilename = null;
338         }
339     }
340
341     /**
342      * Sync the destination file to storage.
343      */
344     private void syncDestination(State state) {
345         FileOutputStream downloadedFileStream = null;
346         try {
347             downloadedFileStream = new FileOutputStream(state.mFilename, true);
348             downloadedFileStream.getFD().sync();
349         } catch (FileNotFoundException ex) {
350             Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
351         } catch (SyncFailedException ex) {
352             Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
353         } catch (IOException ex) {
354             Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
355         } catch (RuntimeException ex) {
356             Log.w(Constants.TAG, "exception while syncing file: ", ex);
357         } finally {
358             if(downloadedFileStream != null) {
359                 try {
360                     downloadedFileStream.close();
361                 } catch (IOException ex) {
362                     Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
363                 } catch (RuntimeException ex) {
364                     Log.w(Constants.TAG, "exception while closing file: ", ex);
365                 }
366             }
367         }
368     }
369
370     /**
371      * Close the destination output stream.
372      */
373     private void closeDestination(State state) {
374         try {
375             // close the file
376             if (state.mStream != null) {
377                 state.mStream.close();
378                 state.mStream = null;
379             }
380         } catch (IOException ex) {
381             if (Constants.LOGV) {
382                 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
383             }
384             // nothing can really be done if the file can't be closed
385         }
386     }
387
388     /**
389      * Check if the download has been paused or canceled, stopping the request appropriately if it
390      * has been.
391      */
392     private void checkPausedOrCanceled(State state) throws StopRequestException {
393         synchronized (mInfo) {
394             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
395                 throw new StopRequestException(
396                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
397             }
398             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
399                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
400             }
401         }
402
403         // if policy has been changed, trigger connectivity check
404         if (mPolicyDirty) {
405             checkConnectivity();
406         }
407     }
408
409     /**
410      * Report download progress through the database if necessary.
411      */
412     private void reportProgress(State state, InnerState innerState) {
413         long now = mSystemFacade.currentTimeMillis();
414         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
415             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
416             ContentValues values = new ContentValues();
417             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
418             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
419             state.mBytesNotified = state.mCurrentBytes;
420             state.mTimeLastNotification = now;
421         }
422     }
423
424     /**
425      * Write a data buffer to the destination file.
426      * @param data buffer containing the data to write
427      * @param bytesRead how many bytes to write from the buffer
428      */
429     private void writeDataToDestination(State state, byte[] data, int bytesRead)
430             throws StopRequestException {
431         for (;;) {
432             try {
433                 if (state.mStream == null) {
434                     state.mStream = new FileOutputStream(state.mFilename, true);
435                 }
436                 mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
437                         bytesRead);
438                 if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) {
439                     state.mStream.write(data, 0, bytesRead);
440                 } else {
441                     byte[] convertedData = mDrmConvertSession.convert(data, bytesRead);
442                     if (convertedData != null) {
443                         state.mStream.write(convertedData, 0, convertedData.length);
444                     } else {
445                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
446                                 "Error converting drm data.");
447                     }
448                 }
449                 return;
450             } catch (IOException ex) {
451                 // couldn't write to file. are we out of space? check.
452                 // TODO this check should only be done once. why is this being done
453                 // in a while(true) loop (see the enclosing statement: for(;;)
454                 if (state.mStream != null) {
455                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
456                 }
457             } finally {
458                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
459                     closeDestination(state);
460                 }
461             }
462         }
463     }
464
465     /**
466      * Called when we've reached the end of the HTTP response stream, to update the database and
467      * check for consistency.
468      */
469     private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
470         ContentValues values = new ContentValues();
471         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
472         if (innerState.mHeaderContentLength == null) {
473             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
474         }
475         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
476
477         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
478                 && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength));
479         if (lengthMismatched) {
480             if (cannotResume(state)) {
481                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
482                         "mismatched content length");
483             } else {
484                 throw new StopRequestException(getFinalStatusForHttpError(state),
485                         "closed socket before end of file");
486             }
487         }
488     }
489
490     private boolean cannotResume(State state) {
491         return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null;
492     }
493
494     /**
495      * Read some data from the HTTP response stream, handling I/O errors.
496      * @param data buffer to use to read data
497      * @param entityStream stream for reading the HTTP response entity
498      * @return the number of bytes actually read or -1 if the end of the stream has been reached
499      */
500     private int readFromResponse(State state, InnerState innerState, byte[] data,
501                                  InputStream entityStream) throws StopRequestException {
502         try {
503             return entityStream.read(data);
504         } catch (IOException ex) {
505             logNetworkState(mInfo.mUid);
506             ContentValues values = new ContentValues();
507             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
508             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
509             if (cannotResume(state)) {
510                 String message = "while reading response: " + ex.toString()
511                 + ", can't resume interrupted download with no ETag";
512                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
513                         message, ex);
514             } else {
515                 throw new StopRequestException(getFinalStatusForHttpError(state),
516                         "while reading response: " + ex.toString(), ex);
517             }
518         }
519     }
520
521     /**
522      * Open a stream for the HTTP response entity, handling I/O errors.
523      * @return an InputStream to read the response entity
524      */
525     private InputStream openResponseEntity(State state, HttpResponse response)
526             throws StopRequestException {
527         try {
528             return response.getEntity().getContent();
529         } catch (IOException ex) {
530             logNetworkState(mInfo.mUid);
531             throw new StopRequestException(getFinalStatusForHttpError(state),
532                     "while getting entity: " + ex.toString(), ex);
533         }
534     }
535
536     private void logNetworkState(int uid) {
537         if (Constants.LOGX) {
538             Log.i(Constants.TAG,
539                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down"));
540         }
541     }
542
543     /**
544      * Read HTTP response headers and take appropriate action, including setting up the destination
545      * file and updating the database.
546      */
547     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
548             throws StopRequestException {
549         if (state.mContinuingDownload) {
550             // ignore response headers on resume requests
551             return;
552         }
553
554         readResponseHeaders(state, innerState, response);
555         if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
556             mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
557             if (mDrmConvertSession == null) {
558                 throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
559                         + state.mMimeType + " can not be converted.");
560             }
561         }
562
563         state.mFilename = Helpers.generateSaveFile(
564                 mContext,
565                 mInfo.mUri,
566                 mInfo.mHint,
567                 innerState.mHeaderContentDisposition,
568                 innerState.mHeaderContentLocation,
569                 state.mMimeType,
570                 mInfo.mDestination,
571                 (innerState.mHeaderContentLength != null) ?
572                         Long.parseLong(innerState.mHeaderContentLength) : 0,
573                 mInfo.mIsPublicApi, mStorageManager);
574         try {
575             state.mStream = new FileOutputStream(state.mFilename);
576         } catch (FileNotFoundException exc) {
577             throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
578                     "while opening destination file: " + exc.toString(), exc);
579         }
580         if (Constants.LOGV) {
581             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
582         }
583
584         updateDatabaseFromHeaders(state, innerState);
585         // check connectivity again now that we know the total size
586         checkConnectivity();
587     }
588
589     /**
590      * Update necessary database fields based on values of HTTP response headers that have been
591      * read.
592      */
593     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
594         ContentValues values = new ContentValues();
595         values.put(Downloads.Impl._DATA, state.mFilename);
596         if (state.mHeaderETag != null) {
597             values.put(Constants.ETAG, state.mHeaderETag);
598         }
599         if (state.mMimeType != null) {
600             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
601         }
602         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
603         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
604     }
605
606     /**
607      * Read headers from the HTTP response and store them into local state.
608      */
609     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
610             throws StopRequestException {
611         Header header = response.getFirstHeader("Content-Disposition");
612         if (header != null) {
613             innerState.mHeaderContentDisposition = header.getValue();
614         }
615         header = response.getFirstHeader("Content-Location");
616         if (header != null) {
617             innerState.mHeaderContentLocation = header.getValue();
618         }
619         if (state.mMimeType == null) {
620             header = response.getFirstHeader("Content-Type");
621             if (header != null) {
622                 state.mMimeType = sanitizeMimeType(header.getValue());
623             }
624         }
625         header = response.getFirstHeader("ETag");
626         if (header != null) {
627             state.mHeaderETag = header.getValue();
628         }
629         String headerTransferEncoding = null;
630         header = response.getFirstHeader("Transfer-Encoding");
631         if (header != null) {
632             headerTransferEncoding = header.getValue();
633         }
634         if (headerTransferEncoding == null) {
635             header = response.getFirstHeader("Content-Length");
636             if (header != null) {
637                 innerState.mHeaderContentLength = header.getValue();
638                 state.mTotalBytes = mInfo.mTotalBytes =
639                         Long.parseLong(innerState.mHeaderContentLength);
640             }
641         } else {
642             // Ignore content-length with transfer-encoding - 2616 4.4 3
643             if (Constants.LOGVV) {
644                 Log.v(Constants.TAG,
645                         "ignoring content-length because of xfer-encoding");
646             }
647         }
648         if (Constants.LOGVV) {
649             Log.v(Constants.TAG, "Content-Disposition: " +
650                     innerState.mHeaderContentDisposition);
651             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
652             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
653             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
654             Log.v(Constants.TAG, "ETag: " + state.mHeaderETag);
655             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
656         }
657
658         boolean noSizeInfo = innerState.mHeaderContentLength == null
659                 && (headerTransferEncoding == null
660                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
661         if (!mInfo.mNoIntegrity && noSizeInfo) {
662             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
663                     "can't know size of download, giving up");
664         }
665     }
666
667     /**
668      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
669      */
670     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
671             throws StopRequestException, RetryDownload {
672         int statusCode = response.getStatusLine().getStatusCode();
673         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
674             handleServiceUnavailable(state, response);
675         }
676         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
677             handleRedirect(state, response, statusCode);
678         }
679
680         if (Constants.LOGV) {
681             Log.i(Constants.TAG, "recevd_status = " + statusCode +
682                     ", mContinuingDownload = " + state.mContinuingDownload);
683         }
684         int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
685         if (statusCode != expectedStatus) {
686             handleOtherStatus(state, innerState, statusCode);
687         }
688     }
689
690     /**
691      * Handle a status that we don't know how to deal with properly.
692      */
693     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
694             throws StopRequestException {
695         if (statusCode == 416) {
696             // range request failed. it should never fail.
697             throw new IllegalStateException("Http Range request failure: totalBytes = " +
698                     state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
699         }
700         int finalStatus;
701         if (Downloads.Impl.isStatusError(statusCode)) {
702             finalStatus = statusCode;
703         } else if (statusCode >= 300 && statusCode < 400) {
704             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
705         } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
706             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
707         } else {
708             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
709         }
710         throw new StopRequestException(finalStatus, "http error " +
711                 statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
712     }
713
714     /**
715      * Handle a 3xx redirect status.
716      */
717     private void handleRedirect(State state, HttpResponse response, int statusCode)
718             throws StopRequestException, RetryDownload {
719         if (Constants.LOGVV) {
720             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
721         }
722         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
723             throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
724                     "too many redirects");
725         }
726         Header header = response.getFirstHeader("Location");
727         if (header == null) {
728             return;
729         }
730         if (Constants.LOGVV) {
731             Log.v(Constants.TAG, "Location :" + header.getValue());
732         }
733
734         String newUri;
735         try {
736             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
737         } catch(URISyntaxException ex) {
738             if (Constants.LOGV) {
739                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
740                         + " for " + mInfo.mUri);
741             }
742             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
743                     "Couldn't resolve redirect URI");
744         }
745         ++state.mRedirectCount;
746         state.mRequestUri = newUri;
747         if (statusCode == 301 || statusCode == 303) {
748             // use the new URI for all future requests (should a retry/resume be necessary)
749             state.mNewUri = newUri;
750         }
751         throw new RetryDownload();
752     }
753
754     /**
755      * Handle a 503 Service Unavailable status by processing the Retry-After header.
756      */
757     private void handleServiceUnavailable(State state, HttpResponse response)
758             throws StopRequestException {
759         if (Constants.LOGVV) {
760             Log.v(Constants.TAG, "got HTTP response code 503");
761         }
762         state.mCountRetry = true;
763         Header header = response.getFirstHeader("Retry-After");
764         if (header != null) {
765            try {
766                if (Constants.LOGVV) {
767                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
768                }
769                state.mRetryAfter = Integer.parseInt(header.getValue());
770                if (state.mRetryAfter < 0) {
771                    state.mRetryAfter = 0;
772                } else {
773                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
774                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
775                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
776                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
777                    }
778                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
779                    state.mRetryAfter *= 1000;
780                }
781            } catch (NumberFormatException ex) {
782                // ignored - retryAfter stays 0 in this case.
783            }
784         }
785         throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
786                 "got 503 Service Unavailable, will retry later");
787     }
788
789     /**
790      * Send the request to the server, handling any I/O exceptions.
791      */
792     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
793             throws StopRequestException {
794         try {
795             return client.execute(request);
796         } catch (IllegalArgumentException ex) {
797             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
798                     "while trying to execute request: " + ex.toString(), ex);
799         } catch (IOException ex) {
800             logNetworkState(mInfo.mUid);
801             throw new StopRequestException(getFinalStatusForHttpError(state),
802                     "while trying to execute request: " + ex.toString(), ex);
803         }
804     }
805
806     private int getFinalStatusForHttpError(State state) {
807         int networkUsable = mInfo.checkCanUseNetwork();
808         if (networkUsable != DownloadInfo.NETWORK_OK) {
809             switch (networkUsable) {
810                 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
811                 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
812                     return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
813                 case DownloadInfo.NETWORK_BLOCKED:
814                     return Downloads.Impl.STATUS_BLOCKED;
815                 default:
816                     return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
817             }
818         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
819             state.mCountRetry = true;
820             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
821         } else {
822             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
823             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
824         }
825     }
826
827     /**
828      * Prepare the destination file to receive data.  If the file already exists, we'll set up
829      * appropriately for resumption.
830      */
831     private void setupDestinationFile(State state, InnerState innerState)
832             throws StopRequestException {
833         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
834             if (Constants.LOGV) {
835                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
836                         ", and state.mFilename: " + state.mFilename);
837             }
838             if (!Helpers.isFilenameValid(state.mFilename,
839                     mStorageManager.getDownloadDataDirectory())) {
840                 // this should never happen
841                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
842                         "found invalid internal destination filename");
843             }
844             // We're resuming a download that got interrupted
845             File f = new File(state.mFilename);
846             if (f.exists()) {
847                 if (Constants.LOGV) {
848                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
849                             ", and state.mFilename: " + state.mFilename);
850                 }
851                 long fileLength = f.length();
852                 if (fileLength == 0) {
853                     // The download hadn't actually started, we can restart from scratch
854                     f.delete();
855                     state.mFilename = null;
856                     if (Constants.LOGV) {
857                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
858                                 ", BUT starting from scratch again: ");
859                     }
860                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
861                     // This should've been caught upon failure
862                     f.delete();
863                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
864                             "Trying to resume a download that can't be resumed");
865                 } else {
866                     // All right, we'll be able to resume this download
867                     if (Constants.LOGV) {
868                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
869                                 ", and starting with file of length: " + fileLength);
870                     }
871                     try {
872                         state.mStream = new FileOutputStream(state.mFilename, true);
873                     } catch (FileNotFoundException exc) {
874                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
875                                 "while opening destination for resuming: " + exc.toString(), exc);
876                     }
877                     state.mCurrentBytes = (int) fileLength;
878                     if (mInfo.mTotalBytes != -1) {
879                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
880                     }
881                     state.mHeaderETag = mInfo.mETag;
882                     state.mContinuingDownload = true;
883                     if (Constants.LOGV) {
884                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
885                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
886                                 ", and setting mContinuingDownload to true: ");
887                     }
888                 }
889             }
890         }
891
892         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
893             closeDestination(state);
894         }
895     }
896
897     /**
898      * Add custom headers for this download to the HTTP request.
899      */
900     private void addRequestHeaders(State state, HttpGet request) {
901         for (Pair<String, String> header : mInfo.getHeaders()) {
902             request.addHeader(header.first, header.second);
903         }
904
905         if (state.mContinuingDownload) {
906             if (state.mHeaderETag != null) {
907                 request.addHeader("If-Match", state.mHeaderETag);
908             }
909             request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
910             if (Constants.LOGV) {
911                 Log.i(Constants.TAG, "Adding Range header: " +
912                         "bytes=" + state.mCurrentBytes + "-");
913                 Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
914             }
915         }
916     }
917
918     /**
919      * Stores information about the completed download, and notifies the initiating application.
920      */
921     private void notifyDownloadCompleted(
922             int status, boolean countRetry, int retryAfter, boolean gotData,
923             String filename, String uri, String mimeType, String errorMsg) {
924         notifyThroughDatabase(
925                 status, countRetry, retryAfter, gotData, filename, uri, mimeType,
926                 errorMsg);
927         if (Downloads.Impl.isStatusCompleted(status)) {
928             mInfo.sendIntentIfRequested();
929         }
930     }
931
932     private void notifyThroughDatabase(
933             int status, boolean countRetry, int retryAfter, boolean gotData,
934             String filename, String uri, String mimeType, String errorMsg) {
935         ContentValues values = new ContentValues();
936         values.put(Downloads.Impl.COLUMN_STATUS, status);
937         values.put(Downloads.Impl._DATA, filename);
938         if (uri != null) {
939             values.put(Downloads.Impl.COLUMN_URI, uri);
940         }
941         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
942         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
943         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
944         if (!countRetry) {
945             values.put(Constants.FAILED_CONNECTIONS, 0);
946         } else if (gotData) {
947             values.put(Constants.FAILED_CONNECTIONS, 1);
948         } else {
949             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
950         }
951         // save the error message. could be useful to developers.
952         if (!TextUtils.isEmpty(errorMsg)) {
953             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
954         }
955         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
956     }
957
958     /**
959      * Clean up a mimeType string so it can be used to dispatch an intent to
960      * view a downloaded asset.
961      * @param mimeType either null or one or more mime types (semi colon separated).
962      * @return null if mimeType was null. Otherwise a string which represents a
963      * single mimetype in lowercase and with surrounding whitespaces trimmed.
964      */
965     private static String sanitizeMimeType(String mimeType) {
966         try {
967             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
968
969             final int semicolonIndex = mimeType.indexOf(';');
970             if (semicolonIndex != -1) {
971                 mimeType = mimeType.substring(0, semicolonIndex);
972             }
973             return mimeType;
974         } catch (NullPointerException npe) {
975             return null;
976         }
977     }
978
979     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
980         @Override
981         public void onUidRulesChanged(int uid, int uidRules) {
982             // only someone like NPMS should only be calling us
983             mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, Constants.TAG);
984
985             if (uid == mInfo.mUid) {
986                 mPolicyDirty = true;
987             }
988         }
989
990         @Override
991         public void onMeteredIfacesChanged(String[] meteredIfaces) {
992             // only someone like NPMS should only be calling us
993             mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, Constants.TAG);
994
995             mPolicyDirty = true;
996         }
997     };
998
999 }