DownloadService.java revision 7dd92fa94df0a13b4592ee636b7aa2b605f6b473
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.Impl.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.Impl.CONTENT_URI,
311                        null, null, null, Downloads.Impl._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.Impl._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.Impl.CONTENT_URI,
495                new String[] { Downloads.Impl._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.Impl.CONTENT_URI,
519                new String[] { Downloads.Impl._ID },
520                Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
521                Downloads.Impl.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.Impl._ID);
530            while (numDelete > 0) {
531                getContentResolver().delete(
532                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI,
533                        cursor.getLong(columnId)), 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.Impl.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.Impl._ID)),
556                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_URI)),
557                cursor.getInt(cursor.getColumnIndexOrThrow(
558                        Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1,
559                cursor.getString(cursor.getColumnIndexOrThrow(
560                        Downloads.Impl.COLUMN_FILE_NAME_HINT)),
561                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA)),
562                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE)),
563                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESTINATION)),
564                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY)),
565                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CONTROL)),
566                cursor.getInt(statusColumn),
567                cursor.getInt(failedColumn),
568                retryRedirect & 0xfffffff,
569                retryRedirect >> 28,
570                cursor.getLong(cursor.getColumnIndexOrThrow(
571                        Downloads.Impl.COLUMN_LAST_MODIFICATION)),
572                cursor.getString(cursor.getColumnIndexOrThrow(
573                        Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE)),
574                cursor.getString(cursor.getColumnIndexOrThrow(
575                        Downloads.Impl.COLUMN_NOTIFICATION_CLASS)),
576                cursor.getString(cursor.getColumnIndexOrThrow(
577                        Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS)),
578                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_COOKIE_DATA)),
579                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_USER_AGENT)),
580                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_REFERER)),
581                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES)),
582                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CURRENT_BYTES)),
583                cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG)),
584                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);
585
586        if (Constants.LOGVV) {
587            Log.v(Constants.TAG, "Service adding new entry");
588            Log.v(Constants.TAG, "ID      : " + info.mId);
589            Log.v(Constants.TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
590            Log.v(Constants.TAG, "NO_INTEG: " + info.mNoIntegrity);
591            Log.v(Constants.TAG, "HINT    : " + info.mHint);
592            Log.v(Constants.TAG, "FILENAME: " + info.mFileName);
593            Log.v(Constants.TAG, "MIMETYPE: " + info.mMimeType);
594            Log.v(Constants.TAG, "DESTINAT: " + info.mDestination);
595            Log.v(Constants.TAG, "VISIBILI: " + info.mVisibility);
596            Log.v(Constants.TAG, "CONTROL : " + info.mControl);
597            Log.v(Constants.TAG, "STATUS  : " + info.mStatus);
598            Log.v(Constants.TAG, "FAILED_C: " + info.mNumFailed);
599            Log.v(Constants.TAG, "RETRY_AF: " + info.mRetryAfter);
600            Log.v(Constants.TAG, "REDIRECT: " + info.mRedirectCount);
601            Log.v(Constants.TAG, "LAST_MOD: " + info.mLastMod);
602            Log.v(Constants.TAG, "PACKAGE : " + info.mPackage);
603            Log.v(Constants.TAG, "CLASS   : " + info.mClass);
604            Log.v(Constants.TAG, "COOKIES : " + ((info.mCookies != null) ? "yes" : "no"));
605            Log.v(Constants.TAG, "AGENT   : " + info.mUserAgent);
606            Log.v(Constants.TAG, "REFERER : " + ((info.mReferer != null) ? "yes" : "no"));
607            Log.v(Constants.TAG, "TOTAL   : " + info.mTotalBytes);
608            Log.v(Constants.TAG, "CURRENT : " + info.mCurrentBytes);
609            Log.v(Constants.TAG, "ETAG    : " + info.mETag);
610            Log.v(Constants.TAG, "SCANNED : " + info.mMediaScanned);
611        }
612
613        mDownloads.add(arrayPos, info);
614
615        if (info.mStatus == 0
616                && (info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
617                    || info.mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE)
618                && info.mMimeType != null
619                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType)) {
620            // Check to see if we are allowed to download this file. Only files
621            // that can be handled by the platform can be downloaded.
622            // special case DRM files, which we should always allow downloading.
623            Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
624
625            // We can provide data as either content: or file: URIs,
626            // so allow both.  (I think it would be nice if we just did
627            // everything as content: URIs)
628            // Actually, right now the download manager's UId restrictions
629            // prevent use from using content: so it's got to be file: or
630            // nothing
631
632            mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mMimeType);
633            ResolveInfo ri = getPackageManager().resolveActivity(mimetypeIntent,
634                    PackageManager.MATCH_DEFAULT_ONLY);
635            //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
636
637            if (ri == null) {
638                if (Config.LOGD) {
639                    Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
640                }
641                info.mStatus = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
642
643                Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
644                ContentValues values = new ContentValues();
645                values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
646                getContentResolver().update(uri, values, null, null);
647                info.sendIntentIfRequested(uri, this);
648                return;
649            }
650        }
651
652        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
653            if (info.isReadyToStart(now)) {
654                if (Constants.LOGV) {
655                    Log.v(Constants.TAG, "Service spawning thread to handle new download " +
656                            info.mId);
657                }
658                if (info.mHasActiveThread) {
659                    throw new IllegalStateException("Multiple threads on same download on insert");
660                }
661                if (info.mStatus != Downloads.Impl.STATUS_RUNNING) {
662                    info.mStatus = Downloads.Impl.STATUS_RUNNING;
663                    ContentValues values = new ContentValues();
664                    values.put(Downloads.Impl.COLUMN_STATUS, info.mStatus);
665                    getContentResolver().update(
666                            ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId),
667                            values, null, null);
668                }
669                DownloadThread downloader = new DownloadThread(this, info);
670                info.mHasActiveThread = true;
671                downloader.start();
672            }
673        } else {
674            if (info.mStatus == 0
675                    || info.mStatus == Downloads.Impl.STATUS_PENDING
676                    || info.mStatus == Downloads.Impl.STATUS_RUNNING) {
677                info.mStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
678                Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
679                ContentValues values = new ContentValues();
680                values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_RUNNING_PAUSED);
681                getContentResolver().update(uri, values, null, null);
682            }
683        }
684    }
685
686    /**
687     * Updates the local copy of the info about a download.
688     */
689    private void updateDownload(
690            Cursor cursor, int arrayPos,
691            boolean networkAvailable, boolean networkRoaming, long now) {
692        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
693        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
694        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
695        info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
696        info.mUri = stringFromCursor(info.mUri, cursor, Downloads.Impl.COLUMN_URI);
697        info.mNoIntegrity = cursor.getInt(cursor.getColumnIndexOrThrow(
698                Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1;
699        info.mHint = stringFromCursor(info.mHint, cursor, Downloads.Impl.COLUMN_FILE_NAME_HINT);
700        info.mFileName = stringFromCursor(info.mFileName, cursor, Downloads.Impl._DATA);
701        info.mMimeType = stringFromCursor(info.mMimeType, cursor, Downloads.Impl.COLUMN_MIME_TYPE);
702        info.mDestination = cursor.getInt(cursor.getColumnIndexOrThrow(
703                Downloads.Impl.COLUMN_DESTINATION));
704        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(
705                Downloads.Impl.COLUMN_VISIBILITY));
706        if (info.mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
707                && newVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
708                && Downloads.Impl.isStatusCompleted(info.mStatus)) {
709            mNotifier.mNotificationMgr.cancel(info.mId);
710        }
711        info.mVisibility = newVisibility;
712        synchronized (info) {
713            info.mControl = cursor.getInt(cursor.getColumnIndexOrThrow(
714                    Downloads.Impl.COLUMN_CONTROL));
715        }
716        int newStatus = cursor.getInt(statusColumn);
717        if (!Downloads.Impl.isStatusCompleted(info.mStatus) &&
718                    Downloads.Impl.isStatusCompleted(newStatus)) {
719            mNotifier.mNotificationMgr.cancel(info.mId);
720        }
721        info.mStatus = newStatus;
722        info.mNumFailed = cursor.getInt(failedColumn);
723        int retryRedirect =
724                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
725        info.mRetryAfter = retryRedirect & 0xfffffff;
726        info.mRedirectCount = retryRedirect >> 28;
727        info.mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
728                Downloads.Impl.COLUMN_LAST_MODIFICATION));
729        info.mPackage = stringFromCursor(
730                info.mPackage, cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
731        info.mClass = stringFromCursor(
732                info.mClass, cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
733        info.mCookies = stringFromCursor(info.mCookies, cursor, Downloads.Impl.COLUMN_COOKIE_DATA);
734        info.mUserAgent = stringFromCursor(
735                info.mUserAgent, cursor, Downloads.Impl.COLUMN_USER_AGENT);
736        info.mReferer = stringFromCursor(info.mReferer, cursor, Downloads.Impl.COLUMN_REFERER);
737        info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
738                Downloads.Impl.COLUMN_TOTAL_BYTES));
739        info.mCurrentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
740                Downloads.Impl.COLUMN_CURRENT_BYTES));
741        info.mETag = stringFromCursor(info.mETag, cursor, Constants.ETAG);
742        info.mMediaScanned =
743                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
744
745        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
746            if (info.isReadyToRestart(now)) {
747                if (Constants.LOGV) {
748                    Log.v(Constants.TAG, "Service spawning thread to handle updated download " +
749                            info.mId);
750                }
751                if (info.mHasActiveThread) {
752                    throw new IllegalStateException("Multiple threads on same download on update");
753                }
754                info.mStatus = Downloads.Impl.STATUS_RUNNING;
755                ContentValues values = new ContentValues();
756                values.put(Downloads.Impl.COLUMN_STATUS, info.mStatus);
757                getContentResolver().update(
758                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId),
759                        values, null, null);
760                DownloadThread downloader = new DownloadThread(this, info);
761                info.mHasActiveThread = true;
762                downloader.start();
763            }
764        }
765    }
766
767    /**
768     * Returns a String that holds the current value of the column,
769     * optimizing for the case where the value hasn't changed.
770     */
771    private String stringFromCursor(String old, Cursor cursor, String column) {
772        int index = cursor.getColumnIndexOrThrow(column);
773        if (old == null) {
774            return cursor.getString(index);
775        }
776        if (mNewChars == null) {
777            mNewChars = new CharArrayBuffer(128);
778        }
779        cursor.copyStringToBuffer(index, mNewChars);
780        int length = mNewChars.sizeCopied;
781        if (length != old.length()) {
782            return cursor.getString(index);
783        }
784        if (oldChars == null || oldChars.sizeCopied < length) {
785            oldChars = new CharArrayBuffer(length);
786        }
787        char[] oldArray = oldChars.data;
788        char[] newArray = mNewChars.data;
789        old.getChars(0, length, oldArray, 0);
790        for (int i = length - 1; i >= 0; --i) {
791            if (oldArray[i] != newArray[i]) {
792                return new String(newArray, 0, length);
793            }
794        }
795        return old;
796    }
797
798    /**
799     * Removes the local copy of the info about a download.
800     */
801    private void deleteDownload(int arrayPos) {
802        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
803        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
804            info.mStatus = Downloads.Impl.STATUS_CANCELED;
805        } else if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL
806                    && info.mFileName != null) {
807            new File(info.mFileName).delete();
808        }
809        mNotifier.mNotificationMgr.cancel(info.mId);
810
811        mDownloads.remove(arrayPos);
812    }
813
814    /**
815     * Returns the amount of time (as measured from the "now" parameter)
816     * at which a download will be active.
817     * 0 = immediately - service should stick around to handle this download.
818     * -1 = never - service can go away without ever waking up.
819     * positive value - service must wake up in the future, as specified in ms from "now"
820     */
821    private long nextAction(int arrayPos, long now) {
822        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
823        if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
824            return -1;
825        }
826        if (info.mStatus != Downloads.Impl.STATUS_RUNNING_PAUSED) {
827            return 0;
828        }
829        if (info.mNumFailed == 0) {
830            return 0;
831        }
832        long when = info.restartTime();
833        if (when <= now) {
834            return 0;
835        }
836        return when - now;
837    }
838
839    /**
840     * Returns whether there's a visible notification for this download
841     */
842    private boolean visibleNotification(int arrayPos) {
843        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
844        return info.hasCompletionNotification();
845    }
846
847    /**
848     * Returns whether a file should be scanned
849     */
850    private boolean shouldScanFile(int arrayPos) {
851        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
852        return !info.mMediaScanned
853                && info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
854                && Downloads.Impl.isStatusSuccess(info.mStatus)
855                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType);
856    }
857
858    /**
859     * Returns whether we have a live connection to the Media Scanner
860     */
861    private boolean mediaScannerConnected() {
862        return mMediaScannerService != null;
863    }
864
865    /**
866     * Attempts to scan the file if necessary.
867     * Returns true if the file has been properly scanned.
868     */
869    private boolean scanFile(Cursor cursor, int arrayPos) {
870        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
871        synchronized (this) {
872            if (mMediaScannerService != null) {
873                try {
874                    if (Constants.LOGV) {
875                        Log.v(Constants.TAG, "Scanning file " + info.mFileName);
876                    }
877                    mMediaScannerService.scanFile(info.mFileName, info.mMimeType);
878                    if (cursor != null) {
879                        ContentValues values = new ContentValues();
880                        values.put(Constants.MEDIA_SCANNED, 1);
881                        getContentResolver().update(ContentUris.withAppendedId(
882                                       Downloads.Impl.CONTENT_URI, cursor.getLong(
883                                               cursor.getColumnIndexOrThrow(Downloads.Impl._ID))),
884                                values, null, null);
885                    }
886                    return true;
887                } catch (RemoteException e) {
888                    if (Config.LOGD) {
889                        Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
890                    }
891                }
892            }
893        }
894        return false;
895    }
896
897}
898