Fix bug with deciding when to restart a download.
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadService.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.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.app.Service;
22 import android.content.ComponentName;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.database.CharArrayBuffer;
31 import android.database.ContentObserver;
32 import android.database.Cursor;
33 import android.drm.mobile1.DrmRawContent;
34 import android.media.IMediaScannerService;
35 import android.net.Uri;
36 import android.os.Environment;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.Process;
40 import android.os.RemoteException;
41 import android.provider.Downloads;
42 import android.util.Config;
43 import android.util.Log;
44
45 import com.google.android.collect.Lists;
46 import com.google.common.annotations.VisibleForTesting;
47
48 import java.io.File;
49 import java.util.ArrayList;
50 import java.util.HashSet;
51 import java.util.Iterator;
52
53
54 /**
55  * Performs the background downloads requested by applications that use the Downloads provider.
56  */
57 public class DownloadService extends Service {
58
59     /* ------------ Constants ------------ */
60
61     /* ------------ Members ------------ */
62
63     /** Observer to get notified when the content observer's data changes */
64     private DownloadManagerContentObserver mObserver;
65
66     /** Class to handle Notification Manager updates */
67     private DownloadNotification mNotifier;
68
69     /**
70      * The Service's view of the list of downloads. This is kept independently
71      * from the content provider, and the Service only initiates downloads
72      * based on this data, so that it can deal with situation where the data
73      * in the content provider changes or disappears.
74      */
75     private ArrayList<DownloadInfo> mDownloads;
76
77     /**
78      * The thread that updates the internal download list from the content
79      * provider.
80      */
81     private UpdateThread mUpdateThread;
82
83     /**
84      * Whether the internal download list should be updated from the content
85      * provider.
86      */
87     private boolean mPendingUpdate;
88
89     /**
90      * The ServiceConnection object that tells us when we're connected to and disconnected from
91      * the Media Scanner
92      */
93     private MediaScannerConnection mMediaScannerConnection;
94
95     private boolean mMediaScannerConnecting;
96
97     /**
98      * The IPC interface to the Media Scanner
99      */
100     private IMediaScannerService mMediaScannerService;
101
102     /**
103      * Array used when extracting strings from content provider
104      */
105     private CharArrayBuffer oldChars;
106
107     /**
108      * Array used when extracting strings from content provider
109      */
110     private CharArrayBuffer mNewChars;
111
112     @VisibleForTesting
113     SystemFacade mSystemFacade;
114
115     /* ------------ Inner Classes ------------ */
116
117     /**
118      * Receives notifications when the data in the content provider changes
119      */
120     private class DownloadManagerContentObserver extends ContentObserver {
121
122         public DownloadManagerContentObserver() {
123             super(new Handler());
124         }
125
126         /**
127          * Receives notification when the data in the observed content
128          * provider changes.
129          */
130         public void onChange(final boolean selfChange) {
131             if (Constants.LOGVV) {
132                 Log.v(Constants.TAG, "Service ContentObserver received notification");
133             }
134             updateFromProvider();
135         }
136
137     }
138
139     /**
140      * Gets called back when the connection to the media
141      * scanner is established or lost.
142      */
143     public class MediaScannerConnection implements ServiceConnection {
144         public void onServiceConnected(ComponentName className, IBinder service) {
145             if (Constants.LOGVV) {
146                 Log.v(Constants.TAG, "Connected to Media Scanner");
147             }
148             mMediaScannerConnecting = false;
149             synchronized (DownloadService.this) {
150                 mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
151                 if (mMediaScannerService != null) {
152                     updateFromProvider();
153                 }
154             }
155         }
156
157         public void disconnectMediaScanner() {
158             synchronized (DownloadService.this) {
159                 if (mMediaScannerService != null) {
160                     mMediaScannerService = null;
161                     if (Constants.LOGVV) {
162                         Log.v(Constants.TAG, "Disconnecting from Media Scanner");
163                     }
164                     try {
165                         unbindService(this);
166                     } catch (IllegalArgumentException ex) {
167                         if (Constants.LOGV) {
168                             Log.v(Constants.TAG, "unbindService threw up: " + ex);
169                         }
170                     }
171                 }
172             }
173         }
174
175         public void onServiceDisconnected(ComponentName className) {
176             if (Constants.LOGVV) {
177                 Log.v(Constants.TAG, "Disconnected from Media Scanner");
178             }
179             synchronized (DownloadService.this) {
180                 mMediaScannerService = null;
181             }
182         }
183     }
184
185     /* ------------ Methods ------------ */
186
187     /**
188      * Returns an IBinder instance when someone wants to connect to this
189      * service. Binding to this service is not allowed.
190      *
191      * @throws UnsupportedOperationException
192      */
193     public IBinder onBind(Intent i) {
194         throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
195     }
196
197     /**
198      * Initializes the service when it is first created
199      */
200     public void onCreate() {
201         super.onCreate();
202         if (Constants.LOGVV) {
203             Log.v(Constants.TAG, "Service onCreate");
204         }
205
206         if (mSystemFacade == null) {
207             mSystemFacade = new RealSystemFacade(this);
208         }
209
210         mDownloads = Lists.newArrayList();
211
212         mObserver = new DownloadManagerContentObserver();
213         getContentResolver().registerContentObserver(Downloads.Impl.CONTENT_URI,
214                 true, mObserver);
215
216         mMediaScannerService = null;
217         mMediaScannerConnecting = false;
218         mMediaScannerConnection = new MediaScannerConnection();
219
220         mNotifier = new DownloadNotification(this);
221         mNotifier.mNotificationMgr.cancelAll();
222         mNotifier.updateNotification();
223
224         trimDatabase();
225         removeSpuriousFiles();
226         updateFromProvider();
227     }
228
229     /**
230      * Responds to a call to startService
231      */
232     public void onStart(Intent intent, int startId) {
233         super.onStart(intent, startId);
234         if (Constants.LOGVV) {
235             Log.v(Constants.TAG, "Service onStart");
236         }
237
238         updateFromProvider();
239     }
240
241     /**
242      * Cleans up when the service is destroyed
243      */
244     public void onDestroy() {
245         getContentResolver().unregisterContentObserver(mObserver);
246         if (Constants.LOGVV) {
247             Log.v(Constants.TAG, "Service onDestroy");
248         }
249         super.onDestroy();
250     }
251
252     /**
253      * Parses data from the content provider into private array
254      */
255     private void updateFromProvider() {
256         synchronized (this) {
257             mPendingUpdate = true;
258             if (mUpdateThread == null) {
259                 mUpdateThread = new UpdateThread();
260                 mUpdateThread.start();
261             }
262         }
263     }
264
265     private class UpdateThread extends Thread {
266         public UpdateThread() {
267             super("Download Service");
268         }
269
270         public void run() {
271             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
272
273             boolean keepService = false;
274             // for each update from the database, remember which download is
275             // supposed to get restarted soonest in the future
276             long wakeUp = Long.MAX_VALUE;
277             for (;;) {
278                 synchronized (DownloadService.this) {
279                     if (mUpdateThread != this) {
280                         throw new IllegalStateException(
281                                 "multiple UpdateThreads in DownloadService");
282                     }
283                     if (!mPendingUpdate) {
284                         mUpdateThread = null;
285                         if (!keepService) {
286                             stopSelf();
287                         }
288                         if (wakeUp != Long.MAX_VALUE) {
289                             AlarmManager alarms =
290                                     (AlarmManager) getSystemService(Context.ALARM_SERVICE);
291                             if (alarms == null) {
292                                 Log.e(Constants.TAG, "couldn't get alarm manager");
293                             } else {
294                                 if (Constants.LOGV) {
295                                     Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
296                                 }
297                                 Intent intent = new Intent(Constants.ACTION_RETRY);
298                                 intent.setClassName("com.android.providers.downloads",
299                                         DownloadReceiver.class.getName());
300                                 alarms.set(
301                                         AlarmManager.RTC_WAKEUP,
302                                         mSystemFacade.currentTimeMillis() + wakeUp,
303                                         PendingIntent.getBroadcast(DownloadService.this, 0, intent,
304                                                 PendingIntent.FLAG_ONE_SHOT));
305                             }
306                         }
307                         oldChars = null;
308                         mNewChars = null;
309                         return;
310                     }
311                     mPendingUpdate = false;
312                 }
313                 long now = mSystemFacade.currentTimeMillis();
314
315                 Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
316                         null, null, null, Downloads.Impl._ID);
317
318                 if (cursor == null) {
319                     // TODO: this doesn't look right, it'd leave the loop in an inconsistent state
320                     return;
321                 }
322
323                 cursor.moveToFirst();
324
325                 int arrayPos = 0;
326
327                 boolean mustScan = false;
328                 keepService = false;
329                 wakeUp = Long.MAX_VALUE;
330
331                 boolean isAfterLast = cursor.isAfterLast();
332
333                 int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
334
335                 /*
336                  * Walk the cursor and the local array to keep them in sync. The key
337                  *     to the algorithm is that the ids are unique and sorted both in
338                  *     the cursor and in the array, so that they can be processed in
339                  *     order in both sources at the same time: at each step, both
340                  *     sources point to the lowest id that hasn't been processed from
341                  *     that source, and the algorithm processes the lowest id from
342                  *     those two possibilities.
343                  * At each step:
344                  * -If the array contains an entry that's not in the cursor, remove the
345                  *     entry, move to next entry in the array.
346                  * -If the array contains an entry that's in the cursor, nothing to do,
347                  *     move to next cursor row and next array entry.
348                  * -If the cursor contains an entry that's not in the array, insert
349                  *     a new entry in the array, move to next cursor row and next
350                  *     array entry.
351                  */
352                 while (!isAfterLast || arrayPos < mDownloads.size()) {
353                     if (isAfterLast) {
354                         // We're beyond the end of the cursor but there's still some
355                         //     stuff in the local array, which can only be junk
356                         if (Constants.LOGVV) {
357                             int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).mId;
358                             Log.v(Constants.TAG, "Array update: trimming " +
359                                     arrayId + " @ "  + arrayPos);
360                         }
361                         if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
362                             scanFile(null, arrayPos);
363                         }
364                         deleteDownload(arrayPos); // this advances in the array
365                     } else {
366                         int id = cursor.getInt(idColumn);
367
368                         if (arrayPos == mDownloads.size()) {
369                             insertDownload(cursor, arrayPos, now);
370                             if (Constants.LOGVV) {
371                                 Log.v(Constants.TAG, "Array update: appending " +
372                                         id + " @ " + arrayPos);
373                             }
374                             if (shouldScanFile(arrayPos)
375                                     && (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
376                                 mustScan = true;
377                                 keepService = true;
378                             }
379                             if (visibleNotification(arrayPos)) {
380                                 keepService = true;
381                             }
382                             long next = nextAction(arrayPos, now);
383                             if (next == 0) {
384                                 keepService = true;
385                             } else if (next > 0 && next < wakeUp) {
386                                 wakeUp = next;
387                             }
388                             ++arrayPos;
389                             cursor.moveToNext();
390                             isAfterLast = cursor.isAfterLast();
391                         } else {
392                             int arrayId = mDownloads.get(arrayPos).mId;
393
394                             if (arrayId < id) {
395                                 // The array entry isn't in the cursor
396                                 if (Constants.LOGVV) {
397                                     Log.v(Constants.TAG, "Array update: removing " + arrayId
398                                             + " @ " + arrayPos);
399                                 }
400                                 if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
401                                     scanFile(null, arrayPos);
402                                 }
403                                 deleteDownload(arrayPos); // this advances in the array
404                             } else if (arrayId == id) {
405                                 // This cursor row already exists in the stored array
406                                 updateDownload(cursor, arrayPos, now);
407                                 if (shouldScanFile(arrayPos)
408                                         && (!mediaScannerConnected()
409                                                 || !scanFile(cursor, arrayPos))) {
410                                     mustScan = true;
411                                     keepService = true;
412                                 }
413                                 if (visibleNotification(arrayPos)) {
414                                     keepService = true;
415                                 }
416                                 long next = nextAction(arrayPos, now);
417                                 if (next == 0) {
418                                     keepService = true;
419                                 } else if (next > 0 && next < wakeUp) {
420                                     wakeUp = next;
421                                 }
422                                 ++arrayPos;
423                                 cursor.moveToNext();
424                                 isAfterLast = cursor.isAfterLast();
425                             } else {
426                                 // This cursor entry didn't exist in the stored array
427                                 if (Constants.LOGVV) {
428                                     Log.v(Constants.TAG, "Array update: inserting " +
429                                             id + " @ " + arrayPos);
430                                 }
431                                 insertDownload(cursor, arrayPos, now);
432                                 if (shouldScanFile(arrayPos)
433                                         && (!mediaScannerConnected()
434                                                 || !scanFile(cursor, arrayPos))) {
435                                     mustScan = true;
436                                     keepService = true;
437                                 }
438                                 if (visibleNotification(arrayPos)) {
439                                     keepService = true;
440                                 }
441                                 long next = nextAction(arrayPos, now);
442                                 if (next == 0) {
443                                     keepService = true;
444                                 } else if (next > 0 && next < wakeUp) {
445                                     wakeUp = next;
446                                 }
447                                 ++arrayPos;
448                                 cursor.moveToNext();
449                                 isAfterLast = cursor.isAfterLast();
450                             }
451                         }
452                     }
453                 }
454
455                 mNotifier.updateNotification();
456
457                 if (mustScan) {
458                     if (!mMediaScannerConnecting) {
459                         Intent intent = new Intent();
460                         intent.setClassName("com.android.providers.media",
461                                 "com.android.providers.media.MediaScannerService");
462                         mMediaScannerConnecting = true;
463                         bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
464                     }
465                 } else {
466                     mMediaScannerConnection.disconnectMediaScanner();
467                 }
468
469                 cursor.close();
470             }
471         }
472     }
473
474     /**
475      * Removes files that may have been left behind in the cache directory
476      */
477     private void removeSpuriousFiles() {
478         File[] files = Environment.getDownloadCacheDirectory().listFiles();
479         if (files == null) {
480             // The cache folder doesn't appear to exist (this is likely the case
481             // when running the simulator).
482             return;
483         }
484         HashSet<String> fileSet = new HashSet();
485         for (int i = 0; i < files.length; i++) {
486             if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
487                 continue;
488             }
489             if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
490                 continue;
491             }
492             fileSet.add(files[i].getPath());
493         }
494
495         Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
496                 new String[] { Downloads.Impl._DATA }, null, null, null);
497         if (cursor != null) {
498             if (cursor.moveToFirst()) {
499                 do {
500                     fileSet.remove(cursor.getString(0));
501                 } while (cursor.moveToNext());
502             }
503             cursor.close();
504         }
505         Iterator<String> iterator = fileSet.iterator();
506         while (iterator.hasNext()) {
507             String filename = iterator.next();
508             if (Constants.LOGV) {
509                 Log.v(Constants.TAG, "deleting spurious file " + filename);
510             }
511             new File(filename).delete();
512         }
513     }
514
515     /**
516      * Drops old rows from the database to prevent it from growing too large
517      */
518     private void trimDatabase() {
519         Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
520                 new String[] { Downloads.Impl._ID },
521                 Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
522                 Downloads.Impl.COLUMN_LAST_MODIFICATION);
523         if (cursor == null) {
524             // This isn't good - if we can't do basic queries in our database, nothing's gonna work
525             Log.e(Constants.TAG, "null cursor in trimDatabase");
526             return;
527         }
528         if (cursor.moveToFirst()) {
529             int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
530             int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
531             while (numDelete > 0) {
532                 getContentResolver().delete(
533                         ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI,
534                         cursor.getLong(columnId)), null, null);
535                 if (!cursor.moveToNext()) {
536                     break;
537                 }
538                 numDelete--;
539             }
540         }
541         cursor.close();
542     }
543
544     /**
545      * Keeps a local copy of the info about a download, and initiates the
546      * download if appropriate.
547      */
548     private void insertDownload(Cursor cursor, int arrayPos, long now) {
549         DownloadInfo info = new DownloadInfo(this, mSystemFacade, cursor);
550
551         if (Constants.LOGVV) {
552             Log.v(Constants.TAG, "Service adding new entry");
553             Log.v(Constants.TAG, "ID      : " + info.mId);
554             Log.v(Constants.TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
555             Log.v(Constants.TAG, "NO_INTEG: " + info.mNoIntegrity);
556             Log.v(Constants.TAG, "HINT    : " + info.mHint);
557             Log.v(Constants.TAG, "FILENAME: " + info.mFileName);
558             Log.v(Constants.TAG, "MIMETYPE: " + info.mMimeType);
559             Log.v(Constants.TAG, "DESTINAT: " + info.mDestination);
560             Log.v(Constants.TAG, "VISIBILI: " + info.mVisibility);
561             Log.v(Constants.TAG, "CONTROL : " + info.mControl);
562             Log.v(Constants.TAG, "STATUS  : " + info.mStatus);
563             Log.v(Constants.TAG, "FAILED_C: " + info.mNumFailed);
564             Log.v(Constants.TAG, "RETRY_AF: " + info.mRetryAfter);
565             Log.v(Constants.TAG, "REDIRECT: " + info.mRedirectCount);
566             Log.v(Constants.TAG, "LAST_MOD: " + info.mLastMod);
567             Log.v(Constants.TAG, "PACKAGE : " + info.mPackage);
568             Log.v(Constants.TAG, "CLASS   : " + info.mClass);
569             Log.v(Constants.TAG, "COOKIES : " + ((info.mCookies != null) ? "yes" : "no"));
570             Log.v(Constants.TAG, "AGENT   : " + info.mUserAgent);
571             Log.v(Constants.TAG, "REFERER : " + ((info.mReferer != null) ? "yes" : "no"));
572             Log.v(Constants.TAG, "TOTAL   : " + info.mTotalBytes);
573             Log.v(Constants.TAG, "CURRENT : " + info.mCurrentBytes);
574             Log.v(Constants.TAG, "ETAG    : " + info.mETag);
575             Log.v(Constants.TAG, "SCANNED : " + info.mMediaScanned);
576         }
577
578         mDownloads.add(arrayPos, info);
579
580         if (info.mStatus == 0
581                 && (info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
582                     || info.mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE)
583                 && info.mMimeType != null
584                 && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType)) {
585             // Check to see if we are allowed to download this file. Only files
586             // that can be handled by the platform can be downloaded.
587             // special case DRM files, which we should always allow downloading.
588             Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
589
590             // We can provide data as either content: or file: URIs,
591             // so allow both.  (I think it would be nice if we just did
592             // everything as content: URIs)
593             // Actually, right now the download manager's UId restrictions
594             // prevent use from using content: so it's got to be file: or
595             // nothing
596
597             mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mMimeType);
598             ResolveInfo ri = getPackageManager().resolveActivity(mimetypeIntent,
599                     PackageManager.MATCH_DEFAULT_ONLY);
600             //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
601
602             if (ri == null) {
603                 if (Config.LOGD) {
604                     Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
605                 }
606                 info.mStatus = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
607
608                 Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
609                 ContentValues values = new ContentValues();
610                 values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
611                 getContentResolver().update(uri, values, null, null);
612                 info.sendIntentIfRequested(uri);
613                 return;
614             }
615         }
616
617         if (info.isReadyToStart(now)) {
618             info.start(now);
619         }
620     }
621
622     /**
623      * Updates the local copy of the info about a download.
624      */
625     private void updateDownload(Cursor cursor, int arrayPos, long now) {
626         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
627         int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
628         int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
629         info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
630         info.mUri = stringFromCursor(info.mUri, cursor, Downloads.Impl.COLUMN_URI);
631         info.mNoIntegrity = cursor.getInt(cursor.getColumnIndexOrThrow(
632                 Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1;
633         info.mHint = stringFromCursor(info.mHint, cursor, Downloads.Impl.COLUMN_FILE_NAME_HINT);
634         info.mFileName = stringFromCursor(info.mFileName, cursor, Downloads.Impl._DATA);
635         info.mMimeType = stringFromCursor(info.mMimeType, cursor, Downloads.Impl.COLUMN_MIME_TYPE);
636         info.mDestination = cursor.getInt(cursor.getColumnIndexOrThrow(
637                 Downloads.Impl.COLUMN_DESTINATION));
638         int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(
639                 Downloads.Impl.COLUMN_VISIBILITY));
640         if (info.mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
641                 && newVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
642                 && Downloads.Impl.isStatusCompleted(info.mStatus)) {
643             mNotifier.mNotificationMgr.cancel(info.mId);
644         }
645         info.mVisibility = newVisibility;
646         synchronized (info) {
647             info.mControl = cursor.getInt(cursor.getColumnIndexOrThrow(
648                     Downloads.Impl.COLUMN_CONTROL));
649         }
650         int newStatus = cursor.getInt(statusColumn);
651         if (!Downloads.Impl.isStatusCompleted(info.mStatus) &&
652                     Downloads.Impl.isStatusCompleted(newStatus)) {
653             mNotifier.mNotificationMgr.cancel(info.mId);
654         }
655         info.mStatus = newStatus;
656         info.mNumFailed = cursor.getInt(failedColumn);
657         int retryRedirect =
658                 cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
659         info.mRetryAfter = retryRedirect & 0xfffffff;
660         info.mRedirectCount = retryRedirect >> 28;
661         info.mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
662                 Downloads.Impl.COLUMN_LAST_MODIFICATION));
663         info.mPackage = stringFromCursor(
664                 info.mPackage, cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
665         info.mClass = stringFromCursor(
666                 info.mClass, cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
667         info.mCookies = stringFromCursor(info.mCookies, cursor, Downloads.Impl.COLUMN_COOKIE_DATA);
668         info.mUserAgent = stringFromCursor(
669                 info.mUserAgent, cursor, Downloads.Impl.COLUMN_USER_AGENT);
670         info.mReferer = stringFromCursor(info.mReferer, cursor, Downloads.Impl.COLUMN_REFERER);
671         info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
672                 Downloads.Impl.COLUMN_TOTAL_BYTES));
673         info.mCurrentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
674                 Downloads.Impl.COLUMN_CURRENT_BYTES));
675         info.mETag = stringFromCursor(info.mETag, cursor, Constants.ETAG);
676         info.mMediaScanned =
677                 cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
678
679         if (info.isReadyToRestart(now)) {
680             info.start(now);
681         }
682     }
683
684     /**
685      * Returns a String that holds the current value of the column,
686      * optimizing for the case where the value hasn't changed.
687      */
688     private String stringFromCursor(String old, Cursor cursor, String column) {
689         int index = cursor.getColumnIndexOrThrow(column);
690         if (old == null) {
691             return cursor.getString(index);
692         }
693         if (mNewChars == null) {
694             mNewChars = new CharArrayBuffer(128);
695         }
696         cursor.copyStringToBuffer(index, mNewChars);
697         int length = mNewChars.sizeCopied;
698         if (length != old.length()) {
699             return cursor.getString(index);
700         }
701         if (oldChars == null || oldChars.sizeCopied < length) {
702             oldChars = new CharArrayBuffer(length);
703         }
704         char[] oldArray = oldChars.data;
705         char[] newArray = mNewChars.data;
706         old.getChars(0, length, oldArray, 0);
707         for (int i = length - 1; i >= 0; --i) {
708             if (oldArray[i] != newArray[i]) {
709                 return new String(newArray, 0, length);
710             }
711         }
712         return old;
713     }
714
715     /**
716      * Removes the local copy of the info about a download.
717      */
718     private void deleteDownload(int arrayPos) {
719         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
720         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
721             info.mStatus = Downloads.Impl.STATUS_CANCELED;
722         } else if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL
723                     && info.mFileName != null) {
724             new File(info.mFileName).delete();
725         }
726         mNotifier.mNotificationMgr.cancel(info.mId);
727
728         mDownloads.remove(arrayPos);
729     }
730
731     /**
732      * Returns the amount of time (as measured from the "now" parameter)
733      * at which a download will be active.
734      * 0 = immediately - service should stick around to handle this download.
735      * -1 = never - service can go away without ever waking up.
736      * positive value - service must wake up in the future, as specified in ms from "now"
737      */
738     private long nextAction(int arrayPos, long now) {
739         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
740         if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
741             return -1;
742         }
743         if (info.mStatus != Downloads.Impl.STATUS_RUNNING_PAUSED) {
744             return 0;
745         }
746         if (info.mNumFailed == 0) {
747             return 0;
748         }
749         long when = info.restartTime();
750         if (when <= now) {
751             return 0;
752         }
753         return when - now;
754     }
755
756     /**
757      * Returns whether there's a visible notification for this download
758      */
759     private boolean visibleNotification(int arrayPos) {
760         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
761         return info.hasCompletionNotification();
762     }
763
764     /**
765      * Returns whether a file should be scanned
766      */
767     private boolean shouldScanFile(int arrayPos) {
768         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
769         return !info.mMediaScanned
770                 && info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
771                 && Downloads.Impl.isStatusSuccess(info.mStatus)
772                 && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType);
773     }
774
775     /**
776      * Returns whether we have a live connection to the Media Scanner
777      */
778     private boolean mediaScannerConnected() {
779         return mMediaScannerService != null;
780     }
781
782     /**
783      * Attempts to scan the file if necessary.
784      * Returns true if the file has been properly scanned.
785      */
786     private boolean scanFile(Cursor cursor, int arrayPos) {
787         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
788         synchronized (this) {
789             if (mMediaScannerService != null) {
790                 try {
791                     if (Constants.LOGV) {
792                         Log.v(Constants.TAG, "Scanning file " + info.mFileName);
793                     }
794                     mMediaScannerService.scanFile(info.mFileName, info.mMimeType);
795                     if (cursor != null) {
796                         ContentValues values = new ContentValues();
797                         values.put(Constants.MEDIA_SCANNED, 1);
798                         getContentResolver().update(ContentUris.withAppendedId(
799                                        Downloads.Impl.CONTENT_URI, cursor.getLong(
800                                                cursor.getColumnIndexOrThrow(Downloads.Impl._ID))),
801                                 values, null, null);
802                     }
803                     return true;
804                 } catch (RemoteException e) {
805                     if (Config.LOGD) {
806                         Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
807                     }
808                 }
809             }
810         }
811         return false;
812     }
813
814 }