Support for max download size that may go over mobile
[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                 // check connectivity just before sending
199                 if (!mInfo.canUseNetwork()) {
200                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
201                     break http_request_loop;
202                 }
203
204                 HttpResponse response;
205                 try {
206                     response = client.execute(request);
207                 } catch (IllegalArgumentException ex) {
208                     if (Constants.LOGV) {
209                         Log.d(Constants.TAG, "Arg exception trying to execute request for " +
210                                 mInfo.mUri + " : " + ex);
211                     } else if (Config.LOGD) {
212                         Log.d(Constants.TAG, "Arg exception trying to execute request for " +
213                                 mInfo.mId + " : " +  ex);
214                     }
215                     finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
216                     request.abort();
217                     break http_request_loop;
218                 } catch (IOException ex) {
219                     if (Constants.LOGX) {
220                         if (Helpers.isNetworkAvailable(mSystemFacade)) {
221                             Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
222                         } else {
223                             Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
224                         }
225                     }
226                     if (!Helpers.isNetworkAvailable(mSystemFacade)) {
227                         finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
228                     } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
229                         finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
230                         countRetry = true;
231                     } else {
232                         if (Constants.LOGV) {
233                             Log.d(Constants.TAG, "IOException trying to execute request for " +
234                                     mInfo.mUri + " : " + ex);
235                         } else if (Config.LOGD) {
236                             Log.d(Constants.TAG, "IOException trying to execute request for " +
237                                     mInfo.mId + " : " + ex);
238                         }
239                         finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
240                     }
241                     request.abort();
242                     break http_request_loop;
243                 }
244
245                 int statusCode = response.getStatusLine().getStatusCode();
246                 if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
247                     if (Constants.LOGVV) {
248                         Log.v(Constants.TAG, "got HTTP response code 503");
249                     }
250                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
251                     countRetry = true;
252                     Header header = response.getFirstHeader("Retry-After");
253                     if (header != null) {
254                        try {
255                            if (Constants.LOGVV) {
256                                Log.v(Constants.TAG, "Retry-After :" + header.getValue());
257                            }
258                            retryAfter = Integer.parseInt(header.getValue());
259                            if (retryAfter < 0) {
260                                retryAfter = 0;
261                            } else {
262                                if (retryAfter < Constants.MIN_RETRY_AFTER) {
263                                    retryAfter = Constants.MIN_RETRY_AFTER;
264                                } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
265                                    retryAfter = Constants.MAX_RETRY_AFTER;
266                                }
267                                retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
268                                retryAfter *= 1000;
269                            }
270                        } catch (NumberFormatException ex) {
271                            // ignored - retryAfter stays 0 in this case.
272                        }
273                     }
274                     request.abort();
275                     break http_request_loop;
276                 }
277                 if (statusCode == 301 ||
278                         statusCode == 302 ||
279                         statusCode == 303 ||
280                         statusCode == 307) {
281                     if (Constants.LOGVV) {
282                         Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
283                     }
284                     if (redirectCount >= Constants.MAX_REDIRECTS) {
285                         if (Constants.LOGV) {
286                             Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
287                                     " at " + mInfo.mUri);
288                         } else if (Config.LOGD) {
289                             Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
290                         }
291                         finalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
292                         request.abort();
293                         break http_request_loop;
294                     }
295                     Header header = response.getFirstHeader("Location");
296                     if (header != null) {
297                         if (Constants.LOGVV) {
298                             Log.v(Constants.TAG, "Location :" + header.getValue());
299                         }
300                         try {
301                             newUri = new URI(mInfo.mUri).
302                                     resolve(new URI(header.getValue())).
303                                     toString();
304                         } catch(URISyntaxException ex) {
305                             if (Constants.LOGV) {
306                                 Log.d(Constants.TAG,
307                                         "Couldn't resolve redirect URI " +
308                                         header.getValue() +
309                                         " for " +
310                                         mInfo.mUri);
311                             } else if (Config.LOGD) {
312                                 Log.d(Constants.TAG,
313                                         "Couldn't resolve redirect URI for download " +
314                                         mInfo.mId);
315                             }
316                             finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
317                             request.abort();
318                             break http_request_loop;
319                         }
320                         ++redirectCount;
321                         finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
322                         request.abort();
323                         break http_request_loop;
324                     }
325                 }
326                 if ((!continuingDownload && statusCode != Downloads.Impl.STATUS_SUCCESS)
327                         || (continuingDownload && statusCode != 206)) {
328                     if (Constants.LOGV) {
329                         Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
330                     } else if (Config.LOGD) {
331                         Log.d(Constants.TAG, "http error " + statusCode + " for download " +
332                                 mInfo.mId);
333                     }
334                     if (Downloads.Impl.isStatusError(statusCode)) {
335                         finalStatus = statusCode;
336                     } else if (statusCode >= 300 && statusCode < 400) {
337                         finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
338                     } else if (continuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
339                         finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
340                     } else {
341                         finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
342                     }
343                     request.abort();
344                     break http_request_loop;
345                 } else {
346                     // Handles the response, saves the file
347                     if (Constants.LOGV) {
348                         Log.v(Constants.TAG, "received response for " + mInfo.mUri);
349                     }
350
351                     if (!continuingDownload) {
352                         Header header = response.getFirstHeader("Accept-Ranges");
353                         if (header != null) {
354                             headerAcceptRanges = header.getValue();
355                         }
356                         header = response.getFirstHeader("Content-Disposition");
357                         if (header != null) {
358                             headerContentDisposition = header.getValue();
359                         }
360                         header = response.getFirstHeader("Content-Location");
361                         if (header != null) {
362                             headerContentLocation = header.getValue();
363                         }
364                         if (mimeType == null) {
365                             header = response.getFirstHeader("Content-Type");
366                             if (header != null) {
367                                 mimeType = sanitizeMimeType(header.getValue());
368                             }
369                         }
370                         header = response.getFirstHeader("ETag");
371                         if (header != null) {
372                             headerETag = header.getValue();
373                         }
374                         header = response.getFirstHeader("Transfer-Encoding");
375                         if (header != null) {
376                             headerTransferEncoding = header.getValue();
377                         }
378                         if (headerTransferEncoding == null) {
379                             header = response.getFirstHeader("Content-Length");
380                             if (header != null) {
381                                 headerContentLength = header.getValue();
382                             }
383                         } else {
384                             // Ignore content-length with transfer-encoding - 2616 4.4 3
385                             if (Constants.LOGVV) {
386                                 Log.v(Constants.TAG,
387                                         "ignoring content-length because of xfer-encoding");
388                             }
389                         }
390                         if (Constants.LOGVV) {
391                             Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
392                             Log.v(Constants.TAG, "Content-Disposition: " +
393                                     headerContentDisposition);
394                             Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
395                             Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
396                             Log.v(Constants.TAG, "Content-Type: " + mimeType);
397                             Log.v(Constants.TAG, "ETag: " + headerETag);
398                             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
399                         }
400
401                         if (!mInfo.mNoIntegrity && headerContentLength == null &&
402                                 (headerTransferEncoding == null
403                                         || !headerTransferEncoding.equalsIgnoreCase("chunked"))
404                                 ) {
405                             if (Config.LOGD) {
406                                 Log.d(Constants.TAG, "can't know size of download, giving up");
407                             }
408                             finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
409                             request.abort();
410                             break http_request_loop;
411                         }
412
413                         DownloadFileInfo fileInfo = Helpers.generateSaveFile(
414                                 mContext,
415                                 mInfo.mUri,
416                                 mInfo.mHint,
417                                 headerContentDisposition,
418                                 headerContentLocation,
419                                 mimeType,
420                                 mInfo.mDestination,
421                                 (headerContentLength != null) ?
422                                         Integer.parseInt(headerContentLength) : 0);
423                         if (fileInfo.mFileName == null) {
424                             finalStatus = fileInfo.mStatus;
425                             request.abort();
426                             break http_request_loop;
427                         }
428                         filename = fileInfo.mFileName;
429                         stream = fileInfo.mStream;
430                         if (Constants.LOGV) {
431                             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + filename);
432                         }
433
434                         ContentValues values = new ContentValues();
435                         values.put(Downloads.Impl._DATA, filename);
436                         if (headerETag != null) {
437                             values.put(Constants.ETAG, headerETag);
438                         }
439                         if (mimeType != null) {
440                             values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
441                         }
442                         int contentLength = -1;
443                         if (headerContentLength != null) {
444                             contentLength = Integer.parseInt(headerContentLength);
445                         }
446                         mInfo.mTotalBytes = contentLength;
447                         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, contentLength);
448                         mContext.getContentResolver().update(contentUri, values, null, null);
449                         // check connectivity again now that we know the total size
450                         if (!mInfo.canUseNetwork()) {
451                             finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
452                             request.abort();
453                             break http_request_loop;
454                         }
455                     }
456
457                     InputStream entityStream;
458                     try {
459                         entityStream = response.getEntity().getContent();
460                     } catch (IOException ex) {
461                         if (Constants.LOGX) {
462                             if (Helpers.isNetworkAvailable(mSystemFacade)) {
463                                 Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
464                             } else {
465                                 Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
466                             }
467                         }
468                         if (!Helpers.isNetworkAvailable(mSystemFacade)) {
469                             finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
470                         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
471                             finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
472                             countRetry = true;
473                         } else {
474                             if (Constants.LOGV) {
475                                 Log.d(Constants.TAG,
476                                         "IOException getting entity for " +
477                                         mInfo.mUri +
478                                         " : " +
479                                         ex);
480                             } else if (Config.LOGD) {
481                                 Log.d(Constants.TAG, "IOException getting entity for download " +
482                                         mInfo.mId + " : " + ex);
483                             }
484                             finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
485                         }
486                         request.abort();
487                         break http_request_loop;
488                     }
489                     for (;;) {
490                         int bytesRead;
491                         try {
492                             bytesRead = entityStream.read(data);
493                         } catch (IOException ex) {
494                             if (Constants.LOGX) {
495                                 if (Helpers.isNetworkAvailable(mSystemFacade)) {
496                                     Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
497                                 } else {
498                                     Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
499                                 }
500                             }
501                             ContentValues values = new ContentValues();
502                             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
503                             mContext.getContentResolver().update(contentUri, values, null, null);
504                             if (!mInfo.mNoIntegrity && headerETag == null) {
505                                 if (Constants.LOGV) {
506                                     Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
507                                             " : " + ex);
508                                 } else if (Config.LOGD) {
509                                     Log.d(Constants.TAG, "download IOException for download " +
510                                             mInfo.mId + " : " + ex);
511                                 }
512                                 if (Config.LOGD) {
513                                     Log.d(Constants.TAG,
514                                             "can't resume interrupted download with no ETag");
515                                 }
516                                 finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
517                             } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
518                                 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
519                             } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
520                                 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
521                                 countRetry = true;
522                             } else {
523                                 if (Constants.LOGV) {
524                                     Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
525                                             " : " + ex);
526                                 } else if (Config.LOGD) {
527                                     Log.d(Constants.TAG, "download IOException for download " +
528                                             mInfo.mId + " : " + ex);
529                                 }
530                                 finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
531                             }
532                             request.abort();
533                             break http_request_loop;
534                         }
535                         if (bytesRead == -1) { // success
536                             ContentValues values = new ContentValues();
537                             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
538                             if (headerContentLength == null) {
539                                 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, bytesSoFar);
540                             }
541                             mContext.getContentResolver().update(contentUri, values, null, null);
542                             if ((headerContentLength != null)
543                                     && (bytesSoFar
544                                             != Integer.parseInt(headerContentLength))) {
545                                 if (!mInfo.mNoIntegrity && headerETag == null) {
546                                     if (Constants.LOGV) {
547                                         Log.d(Constants.TAG, "mismatched content length " +
548                                                 mInfo.mUri);
549                                     } else if (Config.LOGD) {
550                                         Log.d(Constants.TAG, "mismatched content length for " +
551                                                 mInfo.mId);
552                                     }
553                                     finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
554                                 } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
555                                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
556                                 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
557                                     finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
558                                     countRetry = true;
559                                 } else {
560                                     if (Constants.LOGV) {
561                                         Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
562                                     } else if (Config.LOGD) {
563                                         Log.d(Constants.TAG, "closed socket for download " +
564                                                 mInfo.mId);
565                                     }
566                                     finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
567                                 }
568                                 break http_request_loop;
569                             }
570                             break;
571                         }
572                         gotData = true;
573                         for (;;) {
574                             try {
575                                 if (stream == null) {
576                                     stream = new FileOutputStream(filename, true);
577                                 }
578                                 stream.write(data, 0, bytesRead);
579                                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
580                                             && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
581                                             .equalsIgnoreCase(mimeType)) {
582                                     try {
583                                         stream.close();
584                                         stream = null;
585                                     } catch (IOException ex) {
586                                         if (Constants.LOGV) {
587                                             Log.v(Constants.TAG,
588                                                     "exception when closing the file " +
589                                                     "during download : " + ex);
590                                         }
591                                         // nothing can really be done if the file can't be closed
592                                     }
593                                 }
594                                 break;
595                             } catch (IOException ex) {
596                                 if (!Helpers.discardPurgeableFiles(
597                                         mContext, Constants.BUFFER_SIZE)) {
598                                     finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
599                                     break http_request_loop;
600                                 }
601                             }
602                         }
603                         bytesSoFar += bytesRead;
604                         long now = mSystemFacade.currentTimeMillis();
605                         if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
606                                 && now - timeLastNotification
607                                         > Constants.MIN_PROGRESS_TIME) {
608                             ContentValues values = new ContentValues();
609                             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
610                             mContext.getContentResolver().update(
611                                     contentUri, values, null, null);
612                             bytesNotified = bytesSoFar;
613                             timeLastNotification = now;
614                         }
615
616                         if (Constants.LOGVV) {
617                             Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.mUri);
618                         }
619                         synchronized (mInfo) {
620                             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
621                                 if (Constants.LOGV) {
622                                     Log.v(Constants.TAG, "paused " + mInfo.mUri);
623                                 }
624                                 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
625                                 request.abort();
626                                 break http_request_loop;
627                             }
628                         }
629                         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
630                             if (Constants.LOGV) {
631                                 Log.d(Constants.TAG, "canceled " + mInfo.mUri);
632                             } else if (Config.LOGD) {
633                                 // Log.d(Constants.TAG, "canceled id " + mInfo.mId);
634                             }
635                             finalStatus = Downloads.Impl.STATUS_CANCELED;
636                             break http_request_loop;
637                         }
638                     }
639                     if (Constants.LOGV) {
640                         Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
641                     }
642                     finalStatus = Downloads.Impl.STATUS_SUCCESS;
643                 }
644                 break;
645             }
646         } catch (FileNotFoundException ex) {
647             if (Config.LOGD) {
648                 Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " +  ex);
649             }
650             finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
651             // falls through to the code that reports an error
652         } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
653             if (Constants.LOGV) {
654                 Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
655             } else if (Config.LOGD) {
656                 Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
657             }
658             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
659             // falls through to the code that reports an error
660         } finally {
661             mInfo.mHasActiveThread = false;
662             if (wakeLock != null) {
663                 wakeLock.release();
664                 wakeLock = null;
665             }
666             if (client != null) {
667                 client.close();
668                 client = null;
669             }
670             try {
671                 // close the file
672                 if (stream != null) {
673                     stream.close();
674                 }
675             } catch (IOException ex) {
676                 if (Constants.LOGV) {
677                     Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
678                 }
679                 // nothing can really be done if the file can't be closed
680             }
681             if (filename != null) {
682                 // if the download wasn't successful, delete the file
683                 if (Downloads.Impl.isStatusError(finalStatus)) {
684                     new File(filename).delete();
685                     filename = null;
686                 } else if (Downloads.Impl.isStatusSuccess(finalStatus) &&
687                         DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
688                         .equalsIgnoreCase(mimeType)) {
689                     // transfer the file to the DRM content provider
690                     File file = new File(filename);
691                     Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
692                     if (item == null) {
693                         Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
694                         finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
695                     } else {
696                         filename = item.getDataString();
697                         mimeType = item.getType();
698                     }
699
700                     file.delete();
701                 } else if (Downloads.Impl.isStatusSuccess(finalStatus)) {
702                     // make sure the file is readable
703                     FileUtils.setPermissions(filename, 0644, -1, -1);
704
705                     // Sync to storage after completion
706                     FileOutputStream downloadedFileStream = null;
707                     try {
708                         downloadedFileStream = new FileOutputStream(filename, true);
709                         downloadedFileStream.getFD().sync();
710                     } catch (FileNotFoundException ex) {
711                         Log.w(Constants.TAG, "file " + filename + " not found: " + ex);
712                     } catch (SyncFailedException ex) {
713                         Log.w(Constants.TAG, "file " + filename + " sync failed: " + ex);
714                     } catch (IOException ex) {
715                         Log.w(Constants.TAG, "IOException trying to sync " + filename + ": " + ex);
716                     } catch (RuntimeException ex) {
717                         Log.w(Constants.TAG, "exception while syncing file: ", ex);
718                     } finally {
719                         if(downloadedFileStream != null) {
720                             try {
721                                 downloadedFileStream.close();
722                             } catch (IOException ex) {
723                                 Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
724                             } catch (RuntimeException ex) {
725                                 Log.w(Constants.TAG, "exception while closing file: ", ex);
726                             }
727                         }
728                     }
729                 }
730             }
731             notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
732                     gotData, filename, newUri, mimeType);
733         }
734     }
735
736     /**
737      * Add custom headers for this download to the HTTP request.
738      */
739     private void addRequestHeaders(HttpGet request) {
740         for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
741             request.addHeader(header.getKey(), header.getValue());
742         }
743     }
744
745     /**
746      * Stores information about the completed download, and notifies the initiating application.
747      */
748     private void notifyDownloadCompleted(
749             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
750             String filename, String uri, String mimeType) {
751         notifyThroughDatabase(
752                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
753         if (Downloads.Impl.isStatusCompleted(status)) {
754             notifyThroughIntent();
755         }
756     }
757
758     private void notifyThroughDatabase(
759             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
760             String filename, String uri, String mimeType) {
761         ContentValues values = new ContentValues();
762         values.put(Downloads.Impl.COLUMN_STATUS, status);
763         values.put(Downloads.Impl._DATA, filename);
764         if (uri != null) {
765             values.put(Downloads.Impl.COLUMN_URI, uri);
766         }
767         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
768         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
769         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
770         if (!countRetry) {
771             values.put(Constants.FAILED_CONNECTIONS, 0);
772         } else if (gotData) {
773             values.put(Constants.FAILED_CONNECTIONS, 1);
774         } else {
775             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
776         }
777
778         mContext.getContentResolver().update(ContentUris.withAppendedId(
779                 Downloads.Impl.CONTENT_URI, mInfo.mId), values, null, null);
780     }
781
782     /**
783      * Notifies the initiating app if it requested it. That way, it can know that the
784      * download completed even if it's not actively watching the cursor.
785      */
786     private void notifyThroughIntent() {
787         Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
788         mInfo.sendIntentIfRequested(uri);
789     }
790
791     /**
792      * Clean up a mimeType string so it can be used to dispatch an intent to
793      * view a downloaded asset.
794      * @param mimeType either null or one or more mime types (semi colon separated).
795      * @return null if mimeType was null. Otherwise a string which represents a
796      * single mimetype in lowercase and with surrounding whitespaces trimmed.
797      */
798     private String sanitizeMimeType(String mimeType) {
799         try {
800             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
801
802             final int semicolonIndex = mimeType.indexOf(';');
803             if (semicolonIndex != -1) {
804                 mimeType = mimeType.substring(0, semicolonIndex);
805             }
806             return mimeType;
807         } catch (NullPointerException npe) {
808             return null;
809         }
810     }
811 }