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