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