DownloadService.java revision 57f55b3cb4f7e4136cde8d1ea12c1e70ec903362
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
17package com.android.providers.downloads;
18
19import com.google.android.collect.Lists;
20
21import android.app.AlarmManager;
22import android.app.PendingIntent;
23import android.app.Service;
24import android.content.ComponentName;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.content.ServiceConnection;
32import android.database.ContentObserver;
33import android.database.Cursor;
34import android.database.CharArrayBuffer;
35import android.drm.mobile1.DrmRawContent;
36import android.media.IMediaScannerService;
37import android.net.Uri;
38import android.os.RemoteException;
39import android.os.Environment;
40import android.os.Handler;
41import android.os.IBinder;
42import android.os.Process;
43import android.provider.BaseColumns;
44import android.provider.Downloads;
45import android.util.Config;
46import android.util.Log;
47
48import java.io.File;
49import java.util.ArrayList;
50import java.util.HashSet;
51import java.util.Iterator;
52import java.util.List;
53
54
55/**
56 * Performs the background downloads requested by applications that use the Downloads provider.
57 */
58public class DownloadService extends Service {
59
60    /* ------------ Constants ------------ */
61
62    /** Tag used for debugging/logging */
63    private static final String TAG = Constants.TAG;
64
65    /* ------------ Members ------------ */
66
67    /** Observer to get notified when the content observer's data changes */
68    private DownloadManagerContentObserver mObserver;
69
70    /** Class to handle Notification Manager updates */
71    private DownloadNotification mNotifier;
72
73    /**
74     * The Service's view of the list of downloads. This is kept independently
75     * from the content provider, and the Service only initiates downloads
76     * based on this data, so that it can deal with situation where the data
77     * in the content provider changes or disappears.
78     */
79    private ArrayList<DownloadInfo> mDownloads;
80
81    /**
82     * The thread that updates the internal download list from the content
83     * provider.
84     */
85    private UpdateThread updateThread;
86
87    /**
88     * Whether the internal download list should be updated from the content
89     * provider.
90     */
91    private boolean pendingUpdate;
92
93    /**
94     * The ServiceConnection object that tells us when we're connected to and disconnected from
95     * the Media Scanner
96     */
97    private MediaScannerConnection mMediaScannerConnection;
98
99    private boolean mMediaScannerConnecting;
100
101    /**
102     * The IPC interface to the Media Scanner
103     */
104    private IMediaScannerService mMediaScannerService;
105
106    /**
107     * Array used when extracting strings from content provider
108     */
109    private CharArrayBuffer oldChars;
110
111    /**
112     * Array used when extracting strings from content provider
113     */
114    private CharArrayBuffer newChars;
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(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(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(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(TAG, "Service onCreate");
205        }
206
207        mDownloads = Lists.newArrayList();
208
209        mObserver = new DownloadManagerContentObserver();
210        getContentResolver().registerContentObserver(Downloads.CONTENT_URI,
211                true, mObserver);
212
213        mMediaScannerService = null;
214        mMediaScannerConnecting = false;
215        mMediaScannerConnection = new MediaScannerConnection();
216
217        mNotifier = new DownloadNotification(this);
218        mNotifier.mNotificationMgr.cancelAll();
219        mNotifier.updateNotification();
220
221        trimDatabase();
222        removeSpuriousFiles();
223        updateFromProvider();
224    }
225
226    /**
227     * Responds to a call to startService
228     */
229    public void onStart(Intent intent, int startId) {
230        super.onStart(intent, startId);
231        if (Constants.LOGVV) {
232            Log.v(TAG, "Service onStart");
233        }
234
235        updateFromProvider();
236    }
237
238    /**
239     * Cleans up when the service is destroyed
240     */
241    public void onDestroy() {
242        getContentResolver().unregisterContentObserver(mObserver);
243        if (Constants.LOGVV) {
244            Log.v(TAG, "Service onDestroy");
245        }
246        super.onDestroy();
247    }
248
249    /**
250     * Parses data from the content provider into private array
251     */
252    private void updateFromProvider() {
253        synchronized (this) {
254            pendingUpdate = true;
255            if (updateThread == null) {
256                updateThread = new UpdateThread();
257                updateThread.start();
258            }
259        }
260    }
261
262    private class UpdateThread extends Thread {
263        public UpdateThread() {
264            super("Download Service");
265        }
266
267        public void run() {
268            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
269
270            boolean keepService = false;
271            // for each update from the database, remember which download is
272            // supposed to get restarted soonest in the future
273            long wakeUp = Long.MAX_VALUE;
274            for (;;) {
275                synchronized (DownloadService.this) {
276                    if (updateThread != this) {
277                        throw new IllegalStateException(
278                                "multiple UpdateThreads in DownloadService");
279                    }
280                    if (!pendingUpdate) {
281                        updateThread = null;
282                        if (!keepService) {
283                            stopSelf();
284                        }
285                        if (wakeUp != Long.MAX_VALUE) {
286                            AlarmManager alarms =
287                                    (AlarmManager) getSystemService(Context.ALARM_SERVICE);
288                            if (alarms == null) {
289                                Log.e(Constants.TAG, "couldn't get alarm manager");
290                            } else {
291                                if (Constants.LOGV) {
292                                    Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
293                                }
294                                Intent intent = new Intent(Constants.ACTION_RETRY);
295                                intent.setClassName("com.android.providers.downloads",
296                                        DownloadReceiver.class.getName());
297                                alarms.set(
298                                        AlarmManager.RTC_WAKEUP,
299                                        System.currentTimeMillis() + wakeUp,
300                                        PendingIntent.getBroadcast(DownloadService.this, 0, intent,
301                                                PendingIntent.FLAG_ONE_SHOT));
302                            }
303                        }
304                        oldChars = null;
305                        newChars = null;
306                        return;
307                    }
308                    pendingUpdate = false;
309                }
310                boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
311                long now = System.currentTimeMillis();
312
313                Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
314                        null, null, null, BaseColumns._ID);
315
316                if (cursor == null) {
317                    return;
318                }
319
320                cursor.moveToFirst();
321
322                int arrayPos = 0;
323
324                boolean mustScan = false;
325                keepService = false;
326                wakeUp = Long.MAX_VALUE;
327
328                boolean isAfterLast = cursor.isAfterLast();
329
330                int idColumn = cursor.getColumnIndexOrThrow(BaseColumns._ID);
331
332                /*
333                 * Walk the cursor and the local array to keep them in sync. The key
334                 *     to the algorithm is that the ids are unique and sorted both in
335                 *     the cursor and in the array, so that they can be processed in
336                 *     order in both sources at the same time: at each step, both
337                 *     sources point to the lowest id that hasn't been processed from
338                 *     that source, and the algorithm processes the lowest id from
339                 *     those two possibilities.
340                 * At each step:
341                 * -If the array contains an entry that's not in the cursor, remove the
342                 *     entry, move to next entry in the array.
343                 * -If the array contains an entry that's in the cursor, nothing to do,
344                 *     move to next cursor row and next array entry.
345                 * -If the cursor contains an entry that's not in the array, insert
346                 *     a new entry in the array, move to next cursor row and next
347                 *     array entry.
348                 */
349                while (!isAfterLast || arrayPos < mDownloads.size()) {
350                    if (isAfterLast) {
351                        // We're beyond the end of the cursor but there's still some
352                        //     stuff in the local array, which can only be junk
353                        if (Constants.LOGVV) {
354                            int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).id;
355                            Log.v(TAG, "Array update: trimming " + arrayId + " @ "  + arrayPos);
356                        }
357                        if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
358                            scanFile(null, arrayPos);
359                        }
360                        deleteDownload(arrayPos); // this advances in the array
361                    } else {
362                        int id = cursor.getInt(idColumn);
363
364                        if (arrayPos == mDownloads.size()) {
365                            insertDownload(cursor, arrayPos, networkAvailable, now);
366                            if (Constants.LOGVV) {
367                                Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
368                            }
369                            if (shouldScanFile(arrayPos)
370                                    && (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
371                                mustScan = true;
372                                keepService = true;
373                            }
374                            if (visibleNotification(arrayPos)) {
375                                keepService = true;
376                            }
377                            long next = nextAction(arrayPos, now);
378                            if (next == 0) {
379                                keepService = true;
380                            } else if (next > 0 && next < wakeUp) {
381                                wakeUp = next;
382                            }
383                            ++arrayPos;
384                            cursor.moveToNext();
385                            isAfterLast = cursor.isAfterLast();
386                        } else {
387                            int arrayId = mDownloads.get(arrayPos).id;
388
389                            if (arrayId < id) {
390                                // The array entry isn't in the cursor
391                                if (Constants.LOGVV) {
392                                    Log.v(TAG, "Array update: removing " + arrayId
393                                            + " @ " + arrayPos);
394                                }
395                                if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
396                                    scanFile(null, arrayPos);
397                                }
398                                deleteDownload(arrayPos); // this advances in the array
399                            } else if (arrayId == id) {
400                                // This cursor row already exists in the stored array
401                                updateDownload(cursor, arrayPos, networkAvailable, now);
402                                if (shouldScanFile(arrayPos)
403                                        && (!mediaScannerConnected()
404                                                || !scanFile(cursor, arrayPos))) {
405                                    mustScan = true;
406                                    keepService = true;
407                                }
408                                if (visibleNotification(arrayPos)) {
409                                    keepService = true;
410                                }
411                                long next = nextAction(arrayPos, now);
412                                if (next == 0) {
413                                    keepService = true;
414                                } else if (next > 0 && next < wakeUp) {
415                                    wakeUp = next;
416                                }
417                                ++arrayPos;
418                                cursor.moveToNext();
419                                isAfterLast = cursor.isAfterLast();
420                            } else {
421                                // This cursor entry didn't exist in the stored array
422                                if (Constants.LOGVV) {
423                                    Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
424                                }
425                                insertDownload(cursor, arrayPos, networkAvailable, now);
426                                if (shouldScanFile(arrayPos)
427                                        && (!mediaScannerConnected()
428                                                || !scanFile(cursor, arrayPos))) {
429                                    mustScan = true;
430                                    keepService = true;
431                                }
432                                if (visibleNotification(arrayPos)) {
433                                    keepService = true;
434                                }
435                                long next = nextAction(arrayPos, now);
436                                if (next == 0) {
437                                    keepService = true;
438                                } else if (next > 0 && next < wakeUp) {
439                                    wakeUp = next;
440                                }
441                                ++arrayPos;
442                                cursor.moveToNext();
443                                isAfterLast = cursor.isAfterLast();
444                            }
445                        }
446                    }
447                }
448
449                mNotifier.updateNotification();
450
451                if (mustScan) {
452                    if (!mMediaScannerConnecting) {
453                        Intent intent = new Intent();
454                        intent.setClassName("com.android.providers.media",
455                                "com.android.providers.media.MediaScannerService");
456                        mMediaScannerConnecting = true;
457                        bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
458                    }
459                } else {
460                    mMediaScannerConnection.disconnectMediaScanner();
461                }
462
463                if (!cursor.commitUpdates()) {
464                    Log.e(Constants.TAG, "commitUpdates failed in updateFromProvider");
465                }
466                cursor.close();
467            }
468        }
469    }
470
471    /**
472     * Removes files that may have been left behind in the cache directory
473     */
474    private void removeSpuriousFiles() {
475        File[] files = Environment.getDownloadCacheDirectory().listFiles();
476        if (files == null) {
477            // The cache folder doesn't appear to exist (this is likely the case
478            // when running the simulator).
479            return;
480        }
481        HashSet<String> fileSet = new HashSet();
482        for (int i = 0; i < files.length; i++) {
483            if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
484                continue;
485            }
486            if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
487                continue;
488            }
489            fileSet.add(files[i].getPath());
490        }
491
492        Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
493                new String[] { Downloads.FILENAME }, null, null, null);
494        if (cursor != null) {
495            if (cursor.moveToFirst()) {
496                do {
497                    fileSet.remove(cursor.getString(0));
498                } while (cursor.moveToNext());
499            }
500            cursor.close();
501        }
502        Iterator<String> iterator = fileSet.iterator();
503        while (iterator.hasNext()) {
504            String filename = iterator.next();
505            if (Constants.LOGV) {
506                Log.v(Constants.TAG, "deleting spurious file " + filename);
507            }
508            new File(filename).delete();
509        }
510    }
511
512    /**
513     * Drops old rows from the database to prevent it from growing too large
514     */
515    private void trimDatabase() {
516        Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
517                new String[] { Downloads._ID },
518                Downloads.STATUS + " >= 200", null,
519                Downloads.LAST_MODIFICATION);
520        if (cursor == null) {
521            // This isn't good - if we can't do basic queries in our database, nothing's gonna work
522            Log.e(TAG, "null cursor in trimDatabase");
523            return;
524        }
525        if (cursor.moveToFirst()) {
526            while (cursor.getCount() > Constants.MAX_DOWNLOADS) {
527                cursor.deleteRow();
528            }
529        }
530        cursor.close();
531    }
532
533    /**
534     * Keeps a local copy of the info about a download, and initiates the
535     * download if appropriate.
536     */
537    private void insertDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
538        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
539        int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
540        DownloadInfo info = new DownloadInfo(
541                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID)),
542                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.URI)),
543                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD)),
544                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ENTITY)),
545                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1,
546                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME_HINT)),
547                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME)),
548                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1,
549                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.MIMETYPE)),
550                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION)),
551                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1,
552                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY)),
553                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL)),
554                cursor.getInt(statusColumn),
555                cursor.getInt(failedColumn),
556                cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION)),
557                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_PACKAGE)),
558                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_CLASS)),
559                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_EXTRAS)),
560                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COOKIE_DATA)),
561                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.USER_AGENT)),
562                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.REFERER)),
563                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES)),
564                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES)),
565                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ETAG)),
566                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1);
567
568        if (Constants.LOGVV) {
569            Log.v(TAG, "Service adding new entry");
570            Log.v(TAG, "ID      : " + info.id);
571            Log.v(TAG, "URI     : " + ((info.uri != null) ? "yes" : "no"));
572            Log.v(TAG, "METHOD  : " + info.method);
573            Log.v(TAG, "ENTITY  : " + ((info.entity != null) ? "yes" : "no"));
574            Log.v(TAG, "NO_INTEG: " + info.noIntegrity);
575            Log.v(TAG, "HINT    : " + info.hint);
576            Log.v(TAG, "FILENAME: " + info.filename);
577            Log.v(TAG, "SYSIMAGE: " + info.otaUpdate);
578            Log.v(TAG, "MIMETYPE: " + info.mimetype);
579            Log.v(TAG, "DESTINAT: " + info.destination);
580            Log.v(TAG, "NO_SYSTE: " + info.noSystem);
581            Log.v(TAG, "VISIBILI: " + info.visibility);
582            Log.v(TAG, "CONTROL : " + info.control);
583            Log.v(TAG, "STATUS  : " + info.status);
584            Log.v(TAG, "FAILED_C: " + info.numFailed);
585            Log.v(TAG, "LAST_MOD: " + info.lastMod);
586            Log.v(TAG, "PACKAGE : " + info.pckg);
587            Log.v(TAG, "CLASS   : " + info.clazz);
588            Log.v(TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
589            Log.v(TAG, "AGENT   : " + info.userAgent);
590            Log.v(TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
591            Log.v(TAG, "TOTAL   : " + info.totalBytes);
592            Log.v(TAG, "CURRENT : " + info.currentBytes);
593            Log.v(TAG, "ETAG    : " + info.etag);
594            Log.v(TAG, "SCANNED : " + info.mediaScanned);
595        }
596
597        mDownloads.add(arrayPos, info);
598
599        if (info.status == 0
600                && (info.destination == Downloads.DESTINATION_EXTERNAL
601                    || info.destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE)
602                && info.mimetype != null
603                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mimetype)) {
604            // Check to see if we are allowed to download this file. Only files
605            // that can be handled by the platform can be downloaded.
606            // special case DRM files, which we should always allow downloading.
607            Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
608
609            // We can provide data as either content: or file: URIs,
610            // so allow both.  (I think it would be nice if we just did
611            // everything as content: URIs)
612            // Actually, right now the download manager's UId restrictions
613            // prevent use from using content: so it's got to be file: or
614            // nothing
615
616            mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mimetype);
617            List<ResolveInfo> list = getPackageManager().queryIntentActivities(mimetypeIntent,
618                    PackageManager.MATCH_DEFAULT_ONLY);
619            //Log.i(TAG, "*** QUERY " + mimetypeIntent + ": " + list);
620
621            if (list.size() == 0
622                    || (info.noSystem && info.mimetype.equalsIgnoreCase(Constants.MIMETYPE_APK))) {
623                if (Config.LOGD) {
624                    Log.d(Constants.TAG, "no application to handle MIME type " + info.mimetype);
625                }
626                info.status = Downloads.STATUS_NOT_ACCEPTABLE;
627                cursor.updateInt(statusColumn, Downloads.STATUS_NOT_ACCEPTABLE);
628
629                Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + info.id);
630                Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
631                intent.setData(uri);
632                sendBroadcast(intent, "android.permission.ACCESS_DOWNLOAD_DATA");
633                info.sendIntentIfRequested(uri, this);
634                return;
635            }
636        }
637
638        if (networkAvailable) {
639            if (info.isReadyToStart(now)) {
640                if (Constants.LOGV) {
641                    Log.v(TAG, "Service spawning thread to handle new download " + info.id);
642                }
643                if (info.hasActiveThread) {
644                    throw new IllegalStateException("Multiple threads on same download on insert");
645                }
646                if (info.status != Downloads.STATUS_RUNNING) {
647                    info.status = Downloads.STATUS_RUNNING;
648                    ContentValues values = new ContentValues();
649                    values.put(Downloads.STATUS, info.status);
650                    getContentResolver().update(
651                            ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id),
652                            values, null, null);
653                }
654                DownloadThread downloader = new DownloadThread(this, info);
655                info.hasActiveThread = true;
656                downloader.start();
657            }
658        } else {
659            if (info.status == 0
660                    || info.status == Downloads.STATUS_PENDING
661                    || info.status == Downloads.STATUS_RUNNING) {
662                info.status = Downloads.STATUS_RUNNING_PAUSED;
663                cursor.updateInt(statusColumn, Downloads.STATUS_RUNNING_PAUSED);
664            }
665        }
666    }
667
668    /**
669     * Updates the local copy of the info about a download.
670     */
671    private void updateDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
672        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
673        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
674        int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
675        info.id = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID));
676        info.uri = stringFromCursor(info.uri, cursor, Downloads.URI);
677        info.method = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD));
678        info.entity = stringFromCursor(info.entity, cursor, Downloads.ENTITY);
679        info.noIntegrity =
680                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1;
681        info.hint = stringFromCursor(info.hint, cursor, Downloads.FILENAME_HINT);
682        info.filename = stringFromCursor(info.filename, cursor, Downloads.FILENAME);
683        info.otaUpdate = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1;
684        info.mimetype = stringFromCursor(info.mimetype, cursor, Downloads.MIMETYPE);
685        info.destination = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION));
686        info.noSystem =
687                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1;
688        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY));
689        if (info.visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
690                && newVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
691                && Downloads.isStatusCompleted(info.status)) {
692            mNotifier.mNotificationMgr.cancel(info.id);
693        }
694        info.visibility = newVisibility;
695        info.control = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL));
696        int newStatus = cursor.getInt(statusColumn);
697        if (!Downloads.isStatusCompleted(info.status) && Downloads.isStatusCompleted(newStatus)) {
698            mNotifier.mNotificationMgr.cancel(info.id);
699        }
700        info.status = newStatus;
701        info.numFailed = cursor.getInt(failedColumn);
702        info.lastMod = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION));
703        info.pckg = stringFromCursor(info.pckg, cursor, Downloads.NOTIFICATION_PACKAGE);
704        info.clazz = stringFromCursor(info.clazz, cursor, Downloads.NOTIFICATION_CLASS);
705        info.cookies = stringFromCursor(info.cookies, cursor, Downloads.COOKIE_DATA);
706        info.userAgent = stringFromCursor(info.userAgent, cursor, Downloads.USER_AGENT);
707        info.referer = stringFromCursor(info.referer, cursor, Downloads.REFERER);
708        info.totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES));
709        info.currentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES));
710        info.etag = stringFromCursor(info.etag, cursor, Downloads.ETAG);
711        info.mediaScanned =
712                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1;
713
714        if (networkAvailable) {
715            if (info.isReadyToRestart(now)) {
716                if (Constants.LOGV) {
717                    Log.v(TAG, "Service spawning thread to handle updated download " + info.id);
718                }
719                if (info.hasActiveThread) {
720                    throw new IllegalStateException("Multiple threads on same download on update");
721                }
722                info.status = Downloads.STATUS_RUNNING;
723                ContentValues values = new ContentValues();
724                values.put(Downloads.STATUS, info.status);
725                getContentResolver().update(
726                        ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id),
727                        values, null, null);
728                DownloadThread downloader = new DownloadThread(this, info);
729                info.hasActiveThread = true;
730                downloader.start();
731            }
732        }
733    }
734
735    /**
736     * Returns a String that holds the current value of the column,
737     * optimizing for the case where the value hasn't changed.
738     */
739    private String stringFromCursor(String old, Cursor cursor, String column) {
740        int index = cursor.getColumnIndexOrThrow(column);
741        if (old == null) {
742            return cursor.getString(index);
743        }
744        if (newChars == null) {
745            newChars = new CharArrayBuffer(128);
746        }
747        cursor.copyStringToBuffer(index, newChars);
748        int length = newChars.sizeCopied;
749        if (length != old.length()) {
750            return cursor.getString(index);
751        }
752        if (oldChars == null || oldChars.sizeCopied < length) {
753            oldChars = new CharArrayBuffer(length);
754        }
755        char[] oldArray = oldChars.data;
756        char[] newArray = newChars.data;
757        old.getChars(0, length, oldArray, 0);
758        for (int i = length - 1; i >= 0; --i) {
759            if (oldArray[i] != newArray[i]) {
760                return new String(newArray, 0, length);
761            }
762        }
763        return old;
764    }
765
766    /**
767     * Removes the local copy of the info about a download.
768     */
769    private void deleteDownload(int arrayPos) {
770        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
771        if (info.status == Downloads.STATUS_RUNNING) {
772            info.status = Downloads.STATUS_CANCELED;
773        } else if (info.destination != Downloads.DESTINATION_EXTERNAL && info.filename != null) {
774            new File(info.filename).delete();
775        }
776        mNotifier.mNotificationMgr.cancel(info.id);
777
778        mDownloads.remove(arrayPos);
779    }
780
781    /**
782     * Returns the amount of time (as measured from the "now" parameter)
783     * at which a download will be active.
784     * 0 = immediately - service should stick around to handle this download.
785     * -1 = never - service can go away without ever waking up.
786     * positive value - service must wake up in the future, as specified in ms from "now"
787     */
788    private long nextAction(int arrayPos, long now) {
789        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
790        if (Downloads.isStatusCompleted(info.status)) {
791            return -1;
792        }
793        if (info.status != Downloads.STATUS_RUNNING_PAUSED) {
794            return 0;
795        }
796        if (info.numFailed == 0) {
797            return 0;
798        }
799        long when = info.restartTime();
800        if (when <= now) {
801            return 0;
802        }
803        return when - now;
804    }
805
806    /**
807     * Returns whether there's a visible notification for this download
808     */
809    private boolean visibleNotification(int arrayPos) {
810        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
811        return info.hasCompletionNotification();
812    }
813
814    /**
815     * Returns whether a file should be scanned
816     */
817    private boolean shouldScanFile(int arrayPos) {
818        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
819        return !info.mediaScanned
820                && info.destination == Downloads.DESTINATION_EXTERNAL
821                && Downloads.isStatusSuccess(info.status)
822                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mimetype);
823    }
824
825    /**
826     * Returns whether we have a live connection to the Media Scanner
827     */
828    private boolean mediaScannerConnected() {
829        return mMediaScannerService != null;
830    }
831
832    /**
833     * Attempts to scan the file if necessary.
834     * Returns true if the file has been properly scanned.
835     */
836    private boolean scanFile(Cursor cursor, int arrayPos) {
837        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
838        synchronized (this) {
839            if (mMediaScannerService != null) {
840                try {
841                    if (Constants.LOGV) {
842                        Log.v(TAG, "Scanning file " + info.filename);
843                    }
844                    mMediaScannerService.scanFile(info.filename, info.mimetype);
845                    if (cursor != null) {
846                        cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED), 1);
847                    }
848                    return true;
849                } catch (RemoteException e) {
850                    if (Config.LOGD) {
851                        Log.d(TAG, "Failed to scan file " + info.filename);
852                    }
853                }
854            }
855        }
856        return false;
857    }
858
859}
860