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