Support for custom HTTP headers on download requests
[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 android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.drm.mobile1.DrmRawContent;
24 import android.net.Uri;
25 import android.net.http.AndroidHttpClient;
26 import android.os.FileUtils;
27 import android.os.PowerManager;
28 import android.os.Process;
29 import android.provider.Downloads;
30 import android.provider.DrmStore;
31 import android.util.Config;
32 import android.util.Log;
33
34 import org.apache.http.Header;
35 import org.apache.http.HttpResponse;
36 import org.apache.http.client.methods.HttpGet;
37
38 import java.io.File;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.SyncFailedException;
44 import java.net.URI;
45 import java.net.URISyntaxException;
46 import java.util.Locale;
47 import java.util.Map;
48
49 /**
50  * Runs an actual download
51  */
52 public class DownloadThread extends Thread {
53
54     private Context mContext;
55     private DownloadInfo mInfo;
56     private SystemFacade mSystemFacade;
57
58     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
59         mContext = context;
60         mSystemFacade = systemFacade;
61         mInfo = info;
62     }
63
64     /**
65      * Returns the user agent provided by the initiating app, or use the default one
66      */
67     private String userAgent() {
68         String userAgent = mInfo.mUserAgent;
69         if (userAgent != null) {
70         }
71         if (userAgent == null) {
72             userAgent = Constants.DEFAULT_USER_AGENT;
73         }
74         return userAgent;
75     }
76
77     /**
78      * Executes the download in a separate thread
79      */
80     public void run() {
81         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
82
83         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
84         boolean countRetry = false;
85         int retryAfter = 0;
86         int redirectCount = mInfo.mRedirectCount;
87         String newUri = null;
88         boolean gotData = false;
89         String filename = null;
90         String mimeType = sanitizeMimeType(mInfo.mMimeType);
91         FileOutputStream stream = null;
92         AndroidHttpClient client = null;
93         PowerManager.WakeLock wakeLock = null;
94         Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
95
96         try {
97             boolean continuingDownload = false;
98             String headerAcceptRanges = null;
99             String headerContentDisposition = null;
100             String headerContentLength = null;
101             String headerContentLocation = null;
102             String headerETag = null;
103             String headerTransferEncoding = null;
104
105             byte data[] = new byte[Constants.BUFFER_SIZE];
106
107             int bytesSoFar = 0;
108
109             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
110             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
111             wakeLock.acquire();
112
113             filename = mInfo.mFileName;
114             if (filename != null) {
115                 if (!Helpers.isFilenameValid(filename)) {
116                     finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
117                     notifyDownloadCompleted(
118                             finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
119                     return;
120                 }
121                 // We're resuming a download that got interrupted
122                 File f = new File(filename);
123                 if (f.exists()) {
124                     long fileLength = f.length();
125                     if (fileLength == 0) {
126                         // The download hadn't actually started, we can restart from scratch
127                         f.delete();
128                         filename = null;
129                     } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
130                         // Tough luck, that's not a resumable download
131                         if (Config.LOGD) {
132                             Log.d(Constants.TAG,
133                                     "can't resume interrupted non-resumable download");
134                         }
135                         f.delete();
136                         finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
137                         notifyDownloadCompleted(
138                                 finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
139                         return;
140                     } else {
141                         // All right, we'll be able to resume this download
142                         stream = new FileOutputStream(filename, true);
143                         bytesSoFar = (int) fileLength;
144                         if (mInfo.mTotalBytes != -1) {
145                             headerContentLength = Integer.toString(mInfo.mTotalBytes);
146                         }
147                         headerETag = mInfo.mETag;
148                         continuingDownload = true;
149                     }
150                 }
151             }
152
153             int bytesNotified = bytesSoFar;
154             // starting with MIN_VALUE means that the first write will commit
155             //     progress to the database
156             long timeLastNotification = 0;
157
158             client = AndroidHttpClient.newInstance(userAgent(), mContext);
159
160             if (stream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
161                         && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
162                         .equalsIgnoreCase(mimeType)) {
163                 try {
164                     stream.close();
165                     stream = null;
166                 } catch (IOException ex) {
167                     if (Constants.LOGV) {
168                         Log.v(Constants.TAG, "exception when closing the file before download : " +
169                                 ex);
170                     }
171                     // nothing can really be done if the file can't be closed
172                 }
173             }
174
175             /*
176              * This loop is run once for every individual HTTP request that gets sent.
177              * The very first HTTP request is a "virgin" request, while every subsequent
178              * request is done with the original ETag and a byte-range.
179              */
180 http_request_loop:
181             while (true) {
182                 // Prepares the request and fires it.
183                 HttpGet request = new HttpGet(mInfo.mUri);
184
185                 if (Constants.LOGV) {
186                     Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
187                 }
188
189                 addRequestHeaders(request);
190
191                 if (continuingDownload) {
192                     if (headerETag != null) {
193                         request.addHeader("If-Match", headerETag);
194                     }
195                     request.addHeader("Range", "bytes=" + bytesSoFar + "-");
196                 }
197
198                 HttpResponse response;
199                 try {
200                     response = client.execute(request);
201                 } catch (IllegalArgumentException ex) {
202                     if (Constants.LOGV) {
203                         Log.d(Constants.TAG, "Arg exception trying to execute request for " +
204                                 mInfo.mUri + " : " + ex);
205                     } else if (Config.LOGD) {
206                         Log.d(Constants.TAG, "Arg exception trying to execute request for " +
207                                 mInfo.mId + " : " +  ex);
208                     }
209                     finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
210                     request.abort();
211                     break http_request_loop;
212                 } catch (IOException ex) {
213                     if (Constants.LOGX) {
214                         if (Helpers.isNetworkAvailable(mContext)) {
215                             Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
216                         } else {
217                             Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
218                         }
219                     }
220                     if (!Helpers.isNetworkAvailable(mContext)) {
221                         finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
222                     } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
223                         finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
224                         countRetry = true;
225                     } else {
226                         if (Constants.LOGV) {
227                             Log.d(Constants.TAG, "IOException trying to execute request for " +
228                                     mInfo.mUri + " : " + ex);
229                         } else if (Config.LOGD) {
230                             Log.d(Constants.TAG, "IOException trying to execute request for " +
231                                     mInfo.mId + " : " + ex);
232                         }
233                         finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
234                     }
235                     request.abort();
236                     break http_request_loop;
237                 }
238
239                 int statusCode = response.getStatusLine().getStatusCode();
240                 if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
241                     if (Constants.LOGVV) {
242                         Log.v(Constants.TAG, "got HTTP response code 503");
243                     }
244                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
245                     countRetry = true;
246                     Header header = response.getFirstHeader("Retry-After");
247                     if (header != null) {
248                        try {
249                            if (Constants.LOGVV) {
250                                Log.v(Constants.TAG, "Retry-After :" + header.getValue());
251                            }
252                            retryAfter = Integer.parseInt(header.getValue());
253                            if (retryAfter < 0) {
254                                retryAfter = 0;
255                            } else {
256                                if (retryAfter < Constants.MIN_RETRY_AFTER) {
257                                    retryAfter = Constants.MIN_RETRY_AFTER;
258                                } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
259                                    retryAfter = Constants.MAX_RETRY_AFTER;
260                                }
261                                retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
262                                retryAfter *= 1000;
263                            }
264                        } catch (NumberFormatException ex) {
265                            // ignored - retryAfter stays 0 in this case.
266                        }
267                     }
268                     request.abort();
269                     break http_request_loop;
270                 }
271                 if (statusCode == 301 ||
272                         statusCode == 302 ||
273                         statusCode == 303 ||
274                         statusCode == 307) {
275                     if (Constants.LOGVV) {
276                         Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
277                     }
278                     if (redirectCount >= Constants.MAX_REDIRECTS) {
279                         if (Constants.LOGV) {
280                             Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
281                                     " at " + mInfo.mUri);
282                         } else if (Config.LOGD) {
283                             Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
284                         }
285                         finalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
286                         request.abort();
287                         break http_request_loop;
288                     }
289                     Header header = response.getFirstHeader("Location");
290                     if (header != null) {
291                         if (Constants.LOGVV) {
292                             Log.v(Constants.TAG, "Location :" + header.getValue());
293                         }
294                         try {
295                             newUri = new URI(mInfo.mUri).
296                                     resolve(new URI(header.getValue())).
297                                     toString();
298                         } catch(URISyntaxException ex) {
299                             if (Constants.LOGV) {
300                                 Log.d(Constants.TAG,
301                                         "Couldn't resolve redirect URI " +
302                                         header.getValue() +
303                                         " for " +
304                                         mInfo.mUri);
305                             } else if (Config.LOGD) {
306                                 Log.d(Constants.TAG,
307                                         "Couldn't resolve redirect URI for download " +
308                                         mInfo.mId);
309                             }
310                             finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
311                             request.abort();
312                             break http_request_loop;
313                         }
314                         ++redirectCount;
315                         finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
316                         request.abort();
317                         break http_request_loop;
318                     }
319                 }
320                 if ((!continuingDownload && statusCode != Downloads.Impl.STATUS_SUCCESS)
321                         || (continuingDownload && statusCode != 206)) {
322                     if (Constants.LOGV) {
323                         Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
324                     } else if (Config.LOGD) {
325                         Log.d(Constants.TAG, "http error " + statusCode + " for download " +
326                                 mInfo.mId);
327                     }
328                     if (Downloads.Impl.isStatusError(statusCode)) {
329                         finalStatus = statusCode;
330                     } else if (statusCode >= 300 && statusCode < 400) {
331                         finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
332                     } else if (continuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
333                         finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
334                     } else {
335                         finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
336                     }
337                     request.abort();
338                     break http_request_loop;
339                 } else {
340                     // Handles the response, saves the file
341                     if (Constants.LOGV) {
342                         Log.v(Constants.TAG, "received response for " + mInfo.mUri);
343                     }
344
345                     if (!continuingDownload) {
346                         Header header = response.getFirstHeader("Accept-Ranges");
347                         if (header != null) {
348                             headerAcceptRanges = header.getValue();
349                         }
350                         header = response.getFirstHeader("Content-Disposition");
351                         if (header != null) {
352                             headerContentDisposition = header.getValue();
353                         }
354                         header = response.getFirstHeader("Content-Location");
355                         if (header != null) {
356                             headerContentLocation = header.getValue();
357                         }
358                         if (mimeType == null) {
359                             header = response.getFirstHeader("Content-Type");
360                             if (header != null) {
361                                 mimeType = sanitizeMimeType(header.getValue());
362                             }
363                         }
364                         header = response.getFirstHeader("ETag");
365                         if (header != null) {
366                             headerETag = header.getValue();
367                         }
368                         header = response.getFirstHeader("Transfer-Encoding");
369                         if (header != null) {
370                             headerTransferEncoding = header.getValue();
371                         }
372                         if (headerTransferEncoding == null) {
373                             header = response.getFirstHeader("Content-Length");
374                             if (header != null) {
375                                 headerContentLength = header.getValue();
376                             }
377                         } else {
378                             // Ignore content-length with transfer-encoding - 2616 4.4 3
379                             if (Constants.LOGVV) {
380                                 Log.v(Constants.TAG,
381                                         "ignoring content-length because of xfer-encoding");
382                             }
383                         }
384                         if (Constants.LOGVV) {
385                             Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
386                             Log.v(Constants.TAG, "Content-Disposition: " +
387                                     headerContentDisposition);
388                             Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
389                             Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
390                             Log.v(Constants.TAG, "Content-Type: " + mimeType);
391                             Log.v(Constants.TAG, "ETag: " + headerETag);
392                             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
393                         }
394
395                         if (!mInfo.mNoIntegrity && headerContentLength == null &&
396                                 (headerTransferEncoding == null
397                                         || !headerTransferEncoding.equalsIgnoreCase("chunked"))
398                                 ) {
399                             if (Config.LOGD) {
400                                 Log.d(Constants.TAG, "can't know size of download, giving up");
401                             }
402                             finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
403                             request.abort();
404                             break http_request_loop;
405                         }
406
407                         DownloadFileInfo fileInfo = Helpers.generateSaveFile(
408                                 mContext,
409                                 mInfo.mUri,
410                                 mInfo.mHint,
411                                 headerContentDisposition,
412                                 headerContentLocation,
413                                 mimeType,
414                                 mInfo.mDestination,
415                                 (headerContentLength != null) ?
416                                         Integer.parseInt(headerContentLength) : 0);
417                         if (fileInfo.mFileName == null) {
418                             finalStatus = fileInfo.mStatus;
419                             request.abort();
420                             break http_request_loop;
421                         }
422                         filename = fileInfo.mFileName;
423                         stream = fileInfo.mStream;
424                         if (Constants.LOGV) {
425                             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + filename);
426                         }
427
428                         ContentValues values = new ContentValues();
429                         values.put(Downloads.Impl._DATA, filename);
430                         if (headerETag != null) {
431                             values.put(Constants.ETAG, headerETag);
432                         }
433                         if (mimeType != null) {
434                             values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
435                         }
436                         int contentLength = -1;
437                         if (headerContentLength != null) {
438                             contentLength = Integer.parseInt(headerContentLength);
439                         }
440                         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, contentLength);
441                         mContext.getContentResolver().update(contentUri, values, null, null);
442                     }
443
444                     InputStream entityStream;
445                     try {
446                         entityStream = response.getEntity().getContent();
447                     } catch (IOException ex) {
448                         if (Constants.LOGX) {
449                             if (Helpers.isNetworkAvailable(mContext)) {
450                                 Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
451                             } else {
452                                 Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
453                             }
454                         }
455                         if (!Helpers.isNetworkAvailable(mContext)) {
456                             finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
457                         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
458                             finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
459                             countRetry = true;
460                         } else {
461                             if (Constants.LOGV) {
462                                 Log.d(Constants.TAG,
463                                         "IOException getting entity for " +
464                                         mInfo.mUri +
465                                         " : " +
466                                         ex);
467                             } else if (Config.LOGD) {
468                                 Log.d(Constants.TAG, "IOException getting entity for download " +
469                                         mInfo.mId + " : " + ex);
470                             }
471                             finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
472                         }
473                         request.abort();
474                         break http_request_loop;
475                     }
476                     for (;;) {
477                         int bytesRead;
478                         try {
479                             bytesRead = entityStream.read(data);
480                         } catch (IOException ex) {
481                             if (Constants.LOGX) {
482                                 if (Helpers.isNetworkAvailable(mContext)) {
483                                     Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
484                                 } else {
485                                     Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
486                                 }
487                             }
488                             ContentValues values = new ContentValues();
489                             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
490                             mContext.getContentResolver().update(contentUri, values, null, null);
491                             if (!mInfo.mNoIntegrity && headerETag == null) {
492                                 if (Constants.LOGV) {
493                                     Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
494                                             " : " + ex);
495                                 } else if (Config.LOGD) {
496                                     Log.d(Constants.TAG, "download IOException for download " +
497                                             mInfo.mId + " : " + ex);
498                                 }
499                                 if (Config.LOGD) {
500                                     Log.d(Constants.TAG,
501                                             "can't resume interrupted download with no ETag");
502                                 }
503                                 finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
504                             } else if (!Helpers.isNetworkAvailable(mContext)) {
505                                 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
506                             } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
507                                 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
508                                 countRetry = true;
509                             } else {
510                                 if (Constants.LOGV) {
511                                     Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
512                                             " : " + ex);
513                                 } else if (Config.LOGD) {
514                                     Log.d(Constants.TAG, "download IOException for download " +
515                                             mInfo.mId + " : " + ex);
516                                 }
517                                 finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
518                             }
519                             request.abort();
520                             break http_request_loop;
521                         }
522                         if (bytesRead == -1) { // success
523                             ContentValues values = new ContentValues();
524                             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
525                             if (headerContentLength == null) {
526                                 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, bytesSoFar);
527                             }
528                             mContext.getContentResolver().update(contentUri, values, null, null);
529                             if ((headerContentLength != null)
530                                     && (bytesSoFar
531                                             != Integer.parseInt(headerContentLength))) {
532                                 if (!mInfo.mNoIntegrity && headerETag == null) {
533                                     if (Constants.LOGV) {
534                                         Log.d(Constants.TAG, "mismatched content length " +
535                                                 mInfo.mUri);
536                                     } else if (Config.LOGD) {
537                                         Log.d(Constants.TAG, "mismatched content length for " +
538                                                 mInfo.mId);
539                                     }
540                                     finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
541                                 } else if (!Helpers.isNetworkAvailable(mContext)) {
542                                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
543                                 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
544                                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
545                                     countRetry = true;
546                                 } else {
547                                     if (Constants.LOGV) {
548                                         Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
549                                     } else if (Config.LOGD) {
550                                         Log.d(Constants.TAG, "closed socket for download " +
551                                                 mInfo.mId);
552                                     }
553                                     finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
554                                 }
555                                 break http_request_loop;
556                             }
557                             break;
558                         }
559                         gotData = true;
560                         for (;;) {
561                             try {
562                                 if (stream == null) {
563                                     stream = new FileOutputStream(filename, true);
564                                 }
565                                 stream.write(data, 0, bytesRead);
566                                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
567                                             && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
568                                             .equalsIgnoreCase(mimeType)) {
569                                     try {
570                                         stream.close();
571                                         stream = null;
572                                     } catch (IOException ex) {
573                                         if (Constants.LOGV) {
574                                             Log.v(Constants.TAG,
575                                                     "exception when closing the file " +
576                                                     "during download : " + ex);
577                                         }
578                                         // nothing can really be done if the file can't be closed
579                                     }
580                                 }
581                                 break;
582                             } catch (IOException ex) {
583                                 if (!Helpers.discardPurgeableFiles(
584                                         mContext, Constants.BUFFER_SIZE)) {
585                                     finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
586                                     break http_request_loop;
587                                 }
588                             }
589                         }
590                         bytesSoFar += bytesRead;
591                         long now = mSystemFacade.currentTimeMillis();
592                         if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
593                                 && now - timeLastNotification
594                                         > Constants.MIN_PROGRESS_TIME) {
595                             ContentValues values = new ContentValues();
596                             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
597                             mContext.getContentResolver().update(
598                                     contentUri, values, null, null);
599                             bytesNotified = bytesSoFar;
600                             timeLastNotification = now;
601                         }
602
603                         if (Constants.LOGVV) {
604                             Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.mUri);
605                         }
606                         synchronized (mInfo) {
607                             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
608                                 if (Constants.LOGV) {
609                                     Log.v(Constants.TAG, "paused " + mInfo.mUri);
610                                 }
611                                 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
612                                 request.abort();
613                                 break http_request_loop;
614                             }
615                         }
616                         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
617                             if (Constants.LOGV) {
618                                 Log.d(Constants.TAG, "canceled " + mInfo.mUri);
619                             } else if (Config.LOGD) {
620                                 // Log.d(Constants.TAG, "canceled id " + mInfo.mId);
621                             }
622                             finalStatus = Downloads.Impl.STATUS_CANCELED;
623                             break http_request_loop;
624                         }
625                     }
626                     if (Constants.LOGV) {
627                         Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
628                     }
629                     finalStatus = Downloads.Impl.STATUS_SUCCESS;
630                 }
631                 break;
632             }
633         } catch (FileNotFoundException ex) {
634             if (Config.LOGD) {
635                 Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " +  ex);
636             }
637             finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
638             // falls through to the code that reports an error
639         } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
640             if (Constants.LOGV) {
641                 Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
642             } else if (Config.LOGD) {
643                 Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
644             }
645             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
646             // falls through to the code that reports an error
647         } finally {
648             mInfo.mHasActiveThread = false;
649             if (wakeLock != null) {
650                 wakeLock.release();
651                 wakeLock = null;
652             }
653             if (client != null) {
654                 client.close();
655                 client = null;
656             }
657             try {
658                 // close the file
659                 if (stream != null) {
660                     stream.close();
661                 }
662             } catch (IOException ex) {
663                 if (Constants.LOGV) {
664                     Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
665                 }
666                 // nothing can really be done if the file can't be closed
667             }
668             if (filename != null) {
669                 // if the download wasn't successful, delete the file
670                 if (Downloads.Impl.isStatusError(finalStatus)) {
671                     new File(filename).delete();
672                     filename = null;
673                 } else if (Downloads.Impl.isStatusSuccess(finalStatus) &&
674                         DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
675                         .equalsIgnoreCase(mimeType)) {
676                     // transfer the file to the DRM content provider
677                     File file = new File(filename);
678                     Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
679                     if (item == null) {
680                         Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
681                         finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
682                     } else {
683                         filename = item.getDataString();
684                         mimeType = item.getType();
685                     }
686
687                     file.delete();
688                 } else if (Downloads.Impl.isStatusSuccess(finalStatus)) {
689                     // make sure the file is readable
690                     FileUtils.setPermissions(filename, 0644, -1, -1);
691
692                     // Sync to storage after completion
693                     FileOutputStream downloadedFileStream = null;
694                     try {
695                         downloadedFileStream = new FileOutputStream(filename, true);
696                         downloadedFileStream.getFD().sync();
697                     } catch (FileNotFoundException ex) {
698                         Log.w(Constants.TAG, "file " + filename + " not found: " + ex);
699                     } catch (SyncFailedException ex) {
700                         Log.w(Constants.TAG, "file " + filename + " sync failed: " + ex);
701                     } catch (IOException ex) {
702                         Log.w(Constants.TAG, "IOException trying to sync " + filename + ": " + ex);
703                     } catch (RuntimeException ex) {
704                         Log.w(Constants.TAG, "exception while syncing file: ", ex);
705                     } finally {
706                         if(downloadedFileStream != null) {
707                             try {
708                                 downloadedFileStream.close();
709                             } catch (IOException ex) {
710                                 Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
711                             } catch (RuntimeException ex) {
712                                 Log.w(Constants.TAG, "exception while closing file: ", ex);
713                             }
714                         }
715                     }
716                 }
717             }
718             notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
719                     gotData, filename, newUri, mimeType);
720         }
721     }
722
723     /**
724      * Add custom headers for this download to the HTTP request.
725      */
726     private void addRequestHeaders(HttpGet request) {
727         for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
728             request.addHeader(header.getKey(), header.getValue());
729         }
730     }
731
732     /**
733      * Stores information about the completed download, and notifies the initiating application.
734      */
735     private void notifyDownloadCompleted(
736             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
737             String filename, String uri, String mimeType) {
738         notifyThroughDatabase(
739                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
740         if (Downloads.Impl.isStatusCompleted(status)) {
741             notifyThroughIntent();
742         }
743     }
744
745     private void notifyThroughDatabase(
746             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
747             String filename, String uri, String mimeType) {
748         ContentValues values = new ContentValues();
749         values.put(Downloads.Impl.COLUMN_STATUS, status);
750         values.put(Downloads.Impl._DATA, filename);
751         if (uri != null) {
752             values.put(Downloads.Impl.COLUMN_URI, uri);
753         }
754         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
755         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
756         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
757         if (!countRetry) {
758             values.put(Constants.FAILED_CONNECTIONS, 0);
759         } else if (gotData) {
760             values.put(Constants.FAILED_CONNECTIONS, 1);
761         } else {
762             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
763         }
764
765         mContext.getContentResolver().update(ContentUris.withAppendedId(
766                 Downloads.Impl.CONTENT_URI, mInfo.mId), values, null, null);
767     }
768
769     /**
770      * Notifies the initiating app if it requested it. That way, it can know that the
771      * download completed even if it's not actively watching the cursor.
772      */
773     private void notifyThroughIntent() {
774         Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
775         mInfo.sendIntentIfRequested(uri, mContext);
776     }
777
778     /**
779      * Clean up a mimeType string so it can be used to dispatch an intent to
780      * view a downloaded asset.
781      * @param mimeType either null or one or more mime types (semi colon separated).
782      * @return null if mimeType was null. Otherwise a string which represents a
783      * single mimetype in lowercase and with surrounding whitespaces trimmed.
784      */
785     private String sanitizeMimeType(String mimeType) {
786         try {
787             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
788
789             final int semicolonIndex = mimeType.indexOf(';');
790             if (semicolonIndex != -1) {
791                 mimeType = mimeType.substring(0, semicolonIndex);
792             }
793             return mimeType;
794         } catch (NullPointerException npe) {
795             return null;
796         }
797     }
798 }