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