DownloadService.java revision 2061c03400a8e23cfeee5394b7e71db2440a48ed
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                    // TODO: this doesn't look right, it'd leave the loop in an inconsistent state
315                    return;
316                }
317
318                cursor.moveToFirst();
319
320                int arrayPos = 0;
321
322                boolean mustScan = false;
323                keepService = false;
324                wakeUp = Long.MAX_VALUE;
325
326                boolean isAfterLast = cursor.isAfterLast();
327
328                int idColumn = cursor.getColumnIndexOrThrow(Downloads._ID);
329
330                /*
331                 * Walk the cursor and the local array to keep them in sync. The key
332                 *     to the algorithm is that the ids are unique and sorted both in
333                 *     the cursor and in the array, so that they can be processed in
334                 *     order in both sources at the same time: at each step, both
335                 *     sources point to the lowest id that hasn't been processed from
336                 *     that source, and the algorithm processes the lowest id from
337                 *     those two possibilities.
338                 * At each step:
339                 * -If the array contains an entry that's not in the cursor, remove the
340                 *     entry, move to next entry in the array.
341                 * -If the array contains an entry that's in the cursor, nothing to do,
342                 *     move to next cursor row and next array entry.
343                 * -If the cursor contains an entry that's not in the array, insert
344                 *     a new entry in the array, move to next cursor row and next
345                 *     array entry.
346                 */
347                while (!isAfterLast || arrayPos < mDownloads.size()) {
348                    if (isAfterLast) {
349                        // We're beyond the end of the cursor but there's still some
350                        //     stuff in the local array, which can only be junk
351                        if (Constants.LOGVV) {
352                            int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).mId;
353                            Log.v(Constants.TAG, "Array update: trimming " +
354                                    arrayId + " @ "  + arrayPos);
355                        }
356                        if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
357                            scanFile(null, arrayPos);
358                        }
359                        deleteDownload(arrayPos); // this advances in the array
360                    } else {
361                        int id = cursor.getInt(idColumn);
362
363                        if (arrayPos == mDownloads.size()) {
364                            insertDownload(cursor, arrayPos, networkAvailable, networkRoaming, now);
365                            if (Constants.LOGVV) {
366                                Log.v(Constants.TAG, "Array update: appending " +
367                                        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).mId;
388
389                            if (arrayId < id) {
390                                // The array entry isn't in the cursor
391                                if (Constants.LOGVV) {
392                                    Log.v(Constants.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(
402                                        cursor, arrayPos,
403                                        networkAvailable, networkRoaming, now);
404                                if (shouldScanFile(arrayPos)
405                                        && (!mediaScannerConnected()
406                                                || !scanFile(cursor, arrayPos))) {
407                                    mustScan = true;
408                                    keepService = true;
409                                }
410                                if (visibleNotification(arrayPos)) {
411                                    keepService = true;
412                                }
413                                long next = nextAction(arrayPos, now);
414                                if (next == 0) {
415                                    keepService = true;
416                                } else if (next > 0 && next < wakeUp) {
417                                    wakeUp = next;
418                                }
419                                ++arrayPos;
420                                cursor.moveToNext();
421                                isAfterLast = cursor.isAfterLast();
422                            } else {
423                                // This cursor entry didn't exist in the stored array
424                                if (Constants.LOGVV) {
425                                    Log.v(Constants.TAG, "Array update: inserting " +
426                                            id + " @ " + arrayPos);
427                                }
428                                insertDownload(
429                                        cursor, arrayPos,
430                                        networkAvailable, networkRoaming, now);
431                                if (shouldScanFile(arrayPos)
432                                        && (!mediaScannerConnected()
433                                                || !scanFile(cursor, arrayPos))) {
434                                    mustScan = true;
435                                    keepService = true;
436                                }
437                                if (visibleNotification(arrayPos)) {
438                                    keepService = true;
439                                }
440                                long next = nextAction(arrayPos, now);
441                                if (next == 0) {
442                                    keepService = true;
443                                } else if (next > 0 && next < wakeUp) {
444                                    wakeUp = next;
445                                }
446                                ++arrayPos;
447                                cursor.moveToNext();
448                                isAfterLast = cursor.isAfterLast();
449                            }
450                        }
451                    }
452                }
453
454                mNotifier.updateNotification();
455
456                if (mustScan) {
457                    if (!mMediaScannerConnecting) {
458                        Intent intent = new Intent();
459                        intent.setClassName("com.android.providers.media",
460                                "com.android.providers.media.MediaScannerService");
461                        mMediaScannerConnecting = true;
462                        bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
463                    }
464                } else {
465                    mMediaScannerConnection.disconnectMediaScanner();
466                }
467
468                cursor.close();
469            }
470        }
471    }
472
473    /**
474     * Removes files that may have been left behind in the cache directory
475     */
476    private void removeSpuriousFiles() {
477        File[] files = Environment.getDownloadCacheDirectory().listFiles();
478        if (files == null) {
479            // The cache folder doesn't appear to exist (this is likely the case
480            // when running the simulator).
481            return;
482        }
483        HashSet<String> fileSet = new HashSet();
484        for (int i = 0; i < files.length; i++) {
485            if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
486                continue;
487            }
488            if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
489                continue;
490            }
491            fileSet.add(files[i].getPath());
492        }
493
494        Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
495                new String[] { Downloads._DATA }, null, null, null);
496        if (cursor != null) {
497            if (cursor.moveToFirst()) {
498                do {
499                    fileSet.remove(cursor.getString(0));
500                } while (cursor.moveToNext());
501            }
502            cursor.close();
503        }
504        Iterator<String> iterator = fileSet.iterator();
505        while (iterator.hasNext()) {
506            String filename = iterator.next();
507            if (Constants.LOGV) {
508                Log.v(Constants.TAG, "deleting spurious file " + filename);
509            }
510            new File(filename).delete();
511        }
512    }
513
514    /**
515     * Drops old rows from the database to prevent it from growing too large
516     */
517    private void trimDatabase() {
518        Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
519                new String[] { Downloads._ID },
520                Downloads.COLUMN_STATUS + " >= '200'", null,
521                Downloads.COLUMN_LAST_MODIFICATION);
522        if (cursor == null) {
523            // This isn't good - if we can't do basic queries in our database, nothing's gonna work
524            Log.e(Constants.TAG, "null cursor in trimDatabase");
525            return;
526        }
527        if (cursor.moveToFirst()) {
528            int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
529            int columnId = cursor.getColumnIndexOrThrow(Downloads._ID);
530            while (numDelete > 0) {
531                getContentResolver().delete(
532                        ContentUris.withAppendedId(Downloads.CONTENT_URI, cursor.getLong(columnId)),
533                        null, null);
534                if (!cursor.moveToNext()) {
535                    break;
536                }
537                numDelete--;
538            }
539        }
540        cursor.close();
541    }
542
543    /**
544     * Keeps a local copy of the info about a download, and initiates the
545     * download if appropriate.
546     */
547    private void insertDownload(
548            Cursor cursor, int arrayPos,
549            boolean networkAvailable, boolean networkRoaming, long now) {
550        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.COLUMN_STATUS);
551        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
552        int retryRedirect =
553                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
554        DownloadInfo info = new DownloadInfo(
555                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID)),
556                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_URI)),
557                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_NO_INTEGRITY)) == 1,
558                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_FILE_NAME_HINT)),
559                cursor.getString(cursor.getColumnIndexOrThrow(Downloads._DATA)),
560                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_MIME_TYPE)),
561                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_DESTINATION)),
562                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_VISIBILITY)),
563                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_CONTROL)),
564                cursor.getInt(statusColumn),
565                cursor.getInt(failedColumn),
566                retryRedirect & 0xfffffff,
567                retryRedirect >> 28,
568                cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.COLUMN_LAST_MODIFICATION)),
569                cursor.getString(cursor.getColumnIndexOrThrow(
570                        Downloads.COLUMN_NOTIFICATION_PACKAGE)),
571                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_NOTIFICATION_CLASS)),
572                cursor.getString(cursor.getColumnIndexOrThrow(
573                        Downloads.COLUMN_NOTIFICATION_EXTRAS)),
574                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_COOKIE_DATA)),
575                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_USER_AGENT)),
576                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COLUMN_REFERER)),
577                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_TOTAL_BYTES)),
578                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_CURRENT_BYTES)),
579                cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG)),
580                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);
581
582        if (Constants.LOGVV) {
583            Log.v(Constants.TAG, "Service adding new entry");
584            Log.v(Constants.TAG, "ID      : " + info.mId);
585            Log.v(Constants.TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
586            Log.v(Constants.TAG, "NO_INTEG: " + info.mNoIntegrity);
587            Log.v(Constants.TAG, "HINT    : " + info.mHint);
588            Log.v(Constants.TAG, "FILENAME: " + info.mFileName);
589            Log.v(Constants.TAG, "MIMETYPE: " + info.mMimeType);
590            Log.v(Constants.TAG, "DESTINAT: " + info.mDestination);
591            Log.v(Constants.TAG, "VISIBILI: " + info.mVisibility);
592            Log.v(Constants.TAG, "CONTROL : " + info.mControl);
593            Log.v(Constants.TAG, "STATUS  : " + info.mStatus);
594            Log.v(Constants.TAG, "FAILED_C: " + info.mNumFailed);
595            Log.v(Constants.TAG, "RETRY_AF: " + info.mRetryAfter);
596            Log.v(Constants.TAG, "REDIRECT: " + info.mRedirectCount);
597            Log.v(Constants.TAG, "LAST_MOD: " + info.mLastMod);
598            Log.v(Constants.TAG, "PACKAGE : " + info.mPackage);
599            Log.v(Constants.TAG, "CLASS   : " + info.mClass);
600            Log.v(Constants.TAG, "COOKIES : " + ((info.mCookies != null) ? "yes" : "no"));
601            Log.v(Constants.TAG, "AGENT   : " + info.mUserAgent);
602            Log.v(Constants.TAG, "REFERER : " + ((info.mReferer != null) ? "yes" : "no"));
603            Log.v(Constants.TAG, "TOTAL   : " + info.mTotalBytes);
604            Log.v(Constants.TAG, "CURRENT : " + info.mCurrentBytes);
605            Log.v(Constants.TAG, "ETAG    : " + info.mETag);
606            Log.v(Constants.TAG, "SCANNED : " + info.mMediaScanned);
607        }
608
609        mDownloads.add(arrayPos, info);
610
611        if (info.mStatus == 0
612                && (info.mDestination == Downloads.DESTINATION_EXTERNAL
613                    || info.mDestination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE)
614                && info.mMimeType != null
615                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType)) {
616            // Check to see if we are allowed to download this file. Only files
617            // that can be handled by the platform can be downloaded.
618            // special case DRM files, which we should always allow downloading.
619            Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
620
621            // We can provide data as either content: or file: URIs,
622            // so allow both.  (I think it would be nice if we just did
623            // everything as content: URIs)
624            // Actually, right now the download manager's UId restrictions
625            // prevent use from using content: so it's got to be file: or
626            // nothing
627
628            mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mMimeType);
629            ResolveInfo ri = getPackageManager().resolveActivity(mimetypeIntent,
630                    PackageManager.MATCH_DEFAULT_ONLY);
631            //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
632
633            if (ri == null) {
634                if (Config.LOGD) {
635                    Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
636                }
637                info.mStatus = Downloads.STATUS_NOT_ACCEPTABLE;
638
639                Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.mId);
640                ContentValues values = new ContentValues();
641                values.put(Downloads.COLUMN_STATUS, Downloads.STATUS_NOT_ACCEPTABLE);
642                getContentResolver().update(uri, values, null, null);
643                info.sendIntentIfRequested(uri, this);
644                return;
645            }
646        }
647
648        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
649            if (info.isReadyToStart(now)) {
650                if (Constants.LOGV) {
651                    Log.v(Constants.TAG, "Service spawning thread to handle new download " +
652                            info.mId);
653                }
654                if (info.mHasActiveThread) {
655                    throw new IllegalStateException("Multiple threads on same download on insert");
656                }
657                if (info.mStatus != Downloads.STATUS_RUNNING) {
658                    info.mStatus = Downloads.STATUS_RUNNING;
659                    ContentValues values = new ContentValues();
660                    values.put(Downloads.COLUMN_STATUS, info.mStatus);
661                    getContentResolver().update(
662                            ContentUris.withAppendedId(Downloads.CONTENT_URI, info.mId),
663                            values, null, null);
664                }
665                DownloadThread downloader = new DownloadThread(this, info);
666                info.mHasActiveThread = true;
667                downloader.start();
668            }
669        } else {
670            if (info.mStatus == 0
671                    || info.mStatus == Downloads.STATUS_PENDING
672                    || info.mStatus == Downloads.STATUS_RUNNING) {
673                info.mStatus = Downloads.STATUS_RUNNING_PAUSED;
674                Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.mId);
675                ContentValues values = new ContentValues();
676                values.put(Downloads.COLUMN_STATUS, Downloads.STATUS_RUNNING_PAUSED);
677                getContentResolver().update(uri, values, null, null);
678            }
679        }
680    }
681
682    /**
683     * Updates the local copy of the info about a download.
684     */
685    private void updateDownload(
686            Cursor cursor, int arrayPos,
687            boolean networkAvailable, boolean networkRoaming, long now) {
688        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
689        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.COLUMN_STATUS);
690        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
691        info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID));
692        info.mUri = stringFromCursor(info.mUri, cursor, Downloads.COLUMN_URI);
693        info.mNoIntegrity =
694                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_NO_INTEGRITY)) == 1;
695        info.mHint = stringFromCursor(info.mHint, cursor, Downloads.COLUMN_FILE_NAME_HINT);
696        info.mFileName = stringFromCursor(info.mFileName, cursor, Downloads._DATA);
697        info.mMimeType = stringFromCursor(info.mMimeType, cursor, Downloads.COLUMN_MIME_TYPE);
698        info.mDestination = cursor.getInt(cursor.getColumnIndexOrThrow(
699                Downloads.COLUMN_DESTINATION));
700        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(
701                Downloads.COLUMN_VISIBILITY));
702        if (info.mVisibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
703                && newVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
704                && Downloads.isStatusCompleted(info.mStatus)) {
705            mNotifier.mNotificationMgr.cancel(info.mId);
706        }
707        info.mVisibility = newVisibility;
708        synchronized (info) {
709            info.mControl = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.COLUMN_CONTROL));
710        }
711        int newStatus = cursor.getInt(statusColumn);
712        if (!Downloads.isStatusCompleted(info.mStatus) && Downloads.isStatusCompleted(newStatus)) {
713            mNotifier.mNotificationMgr.cancel(info.mId);
714        }
715        info.mStatus = newStatus;
716        info.mNumFailed = cursor.getInt(failedColumn);
717        int retryRedirect =
718                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
719        info.mRetryAfter = retryRedirect & 0xfffffff;
720        info.mRedirectCount = retryRedirect >> 28;
721        info.mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
722                Downloads.COLUMN_LAST_MODIFICATION));
723        info.mPackage = stringFromCursor(
724                info.mPackage, cursor, Downloads.COLUMN_NOTIFICATION_PACKAGE);
725        info.mClass = stringFromCursor(info.mClass, cursor, Downloads.COLUMN_NOTIFICATION_CLASS);
726        info.mCookies = stringFromCursor(info.mCookies, cursor, Downloads.COLUMN_COOKIE_DATA);
727        info.mUserAgent = stringFromCursor(info.mUserAgent, cursor, Downloads.COLUMN_USER_AGENT);
728        info.mReferer = stringFromCursor(info.mReferer, cursor, Downloads.COLUMN_REFERER);
729        info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
730                Downloads.COLUMN_TOTAL_BYTES));
731        info.mCurrentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
732                Downloads.COLUMN_CURRENT_BYTES));
733        info.mETag = stringFromCursor(info.mETag, cursor, Constants.ETAG);
734        info.mMediaScanned =
735                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
736
737        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
738            if (info.isReadyToRestart(now)) {
739                if (Constants.LOGV) {
740                    Log.v(Constants.TAG, "Service spawning thread to handle updated download " +
741                            info.mId);
742                }
743                if (info.mHasActiveThread) {
744                    throw new IllegalStateException("Multiple threads on same download on update");
745                }
746                info.mStatus = Downloads.STATUS_RUNNING;
747                ContentValues values = new ContentValues();
748                values.put(Downloads.COLUMN_STATUS, info.mStatus);
749                getContentResolver().update(
750                        ContentUris.withAppendedId(Downloads.CONTENT_URI, info.mId),
751                        values, null, null);
752                DownloadThread downloader = new DownloadThread(this, info);
753                info.mHasActiveThread = true;
754                downloader.start();
755            }
756        }
757    }
758
759    /**
760     * Returns a String that holds the current value of the column,
761     * optimizing for the case where the value hasn't changed.
762     */
763    private String stringFromCursor(String old, Cursor cursor, String column) {
764        int index = cursor.getColumnIndexOrThrow(column);
765        if (old == null) {
766            return cursor.getString(index);
767        }
768        if (mNewChars == null) {
769            mNewChars = new CharArrayBuffer(128);
770        }
771        cursor.copyStringToBuffer(index, mNewChars);
772        int length = mNewChars.sizeCopied;
773        if (length != old.length()) {
774            return cursor.getString(index);
775        }
776        if (oldChars == null || oldChars.sizeCopied < length) {
777            oldChars = new CharArrayBuffer(length);
778        }
779        char[] oldArray = oldChars.data;
780        char[] newArray = mNewChars.data;
781        old.getChars(0, length, oldArray, 0);
782        for (int i = length - 1; i >= 0; --i) {
783            if (oldArray[i] != newArray[i]) {
784                return new String(newArray, 0, length);
785            }
786        }
787        return old;
788    }
789
790    /**
791     * Removes the local copy of the info about a download.
792     */
793    private void deleteDownload(int arrayPos) {
794        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
795        if (info.mStatus == Downloads.STATUS_RUNNING) {
796            info.mStatus = Downloads.STATUS_CANCELED;
797        } else if (info.mDestination != Downloads.DESTINATION_EXTERNAL && info.mFileName != null) {
798            new File(info.mFileName).delete();
799        }
800        mNotifier.mNotificationMgr.cancel(info.mId);
801
802        mDownloads.remove(arrayPos);
803    }
804
805    /**
806     * Returns the amount of time (as measured from the "now" parameter)
807     * at which a download will be active.
808     * 0 = immediately - service should stick around to handle this download.
809     * -1 = never - service can go away without ever waking up.
810     * positive value - service must wake up in the future, as specified in ms from "now"
811     */
812    private long nextAction(int arrayPos, long now) {
813        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
814        if (Downloads.isStatusCompleted(info.mStatus)) {
815            return -1;
816        }
817        if (info.mStatus != Downloads.STATUS_RUNNING_PAUSED) {
818            return 0;
819        }
820        if (info.mNumFailed == 0) {
821            return 0;
822        }
823        long when = info.restartTime();
824        if (when <= now) {
825            return 0;
826        }
827        return when - now;
828    }
829
830    /**
831     * Returns whether there's a visible notification for this download
832     */
833    private boolean visibleNotification(int arrayPos) {
834        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
835        return info.hasCompletionNotification();
836    }
837
838    /**
839     * Returns whether a file should be scanned
840     */
841    private boolean shouldScanFile(int arrayPos) {
842        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
843        return !info.mMediaScanned
844                && info.mDestination == Downloads.DESTINATION_EXTERNAL
845                && Downloads.isStatusSuccess(info.mStatus)
846                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType);
847    }
848
849    /**
850     * Returns whether we have a live connection to the Media Scanner
851     */
852    private boolean mediaScannerConnected() {
853        return mMediaScannerService != null;
854    }
855
856    /**
857     * Attempts to scan the file if necessary.
858     * Returns true if the file has been properly scanned.
859     */
860    private boolean scanFile(Cursor cursor, int arrayPos) {
861        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
862        synchronized (this) {
863            if (mMediaScannerService != null) {
864                try {
865                    if (Constants.LOGV) {
866                        Log.v(Constants.TAG, "Scanning file " + info.mFileName);
867                    }
868                    mMediaScannerService.scanFile(info.mFileName, info.mMimeType);
869                    if (cursor != null) {
870                        ContentValues values = new ContentValues();
871                        values.put(Constants.MEDIA_SCANNED, 1);
872                        getContentResolver().update(
873                                ContentUris.withAppendedId(Downloads.CONTENT_URI,
874                                       cursor.getLong(cursor.getColumnIndexOrThrow(Downloads._ID))),
875                                values, null, null);
876                    }
877                    return true;
878                } catch (RemoteException e) {
879                    if (Config.LOGD) {
880                        Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
881                    }
882                }
883            }
884        }
885        return false;
886    }
887
888}
889