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