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