DownloadService.java revision 04f09085f85d094207d22ae8969102bd48178180
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.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.Intent;
28import android.content.ServiceConnection;
29import android.database.ContentObserver;
30import android.database.Cursor;
31import android.media.IMediaScannerListener;
32import android.media.IMediaScannerService;
33import android.net.Uri;
34import android.os.Environment;
35import android.os.Handler;
36import android.os.IBinder;
37import android.os.Process;
38import android.os.RemoteException;
39import android.provider.Downloads;
40import android.text.TextUtils;
41import android.util.Log;
42
43import com.google.android.collect.Maps;
44import com.google.common.annotations.VisibleForTesting;
45
46import java.io.File;
47import java.util.HashSet;
48import java.util.Iterator;
49import java.util.Map;
50import java.util.Set;
51
52
53/**
54 * Performs the background downloads requested by applications that use the Downloads provider.
55 */
56public class DownloadService extends Service {
57    /** amount of time to wait to connect to MediaScannerService before timing out */
58    private static final long WAIT_TIMEOUT = 10 * 1000;
59
60    /** Observer to get notified when the content observer's data changes */
61    private DownloadManagerContentObserver mObserver;
62
63    /** Class to handle Notification Manager updates */
64    private DownloadNotification mNotifier;
65
66    /**
67     * The Service's view of the list of downloads, mapping download IDs to the corresponding info
68     * object. This is kept independently from the content provider, and the Service only initiates
69     * downloads based on this data, so that it can deal with situation where the data in the
70     * content provider changes or disappears.
71     */
72    private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
73
74    /**
75     * The thread that updates the internal download list from the content
76     * provider.
77     */
78    @VisibleForTesting
79    UpdateThread mUpdateThread;
80
81    /**
82     * Whether the internal download list should be updated from the content
83     * provider.
84     */
85    private boolean mPendingUpdate;
86
87    /**
88     * The ServiceConnection object that tells us when we're connected to and disconnected from
89     * the Media Scanner
90     */
91    private MediaScannerConnection mMediaScannerConnection;
92
93    private boolean mMediaScannerConnecting;
94
95    /**
96     * The IPC interface to the Media Scanner
97     */
98    private IMediaScannerService mMediaScannerService;
99
100    @VisibleForTesting
101    SystemFacade mSystemFacade;
102
103    /**
104     * Receives notifications when the data in the content provider changes
105     */
106    private class DownloadManagerContentObserver extends ContentObserver {
107
108        public DownloadManagerContentObserver() {
109            super(new Handler());
110        }
111
112        /**
113         * Receives notification when the data in the observed content
114         * provider changes.
115         */
116        @Override
117        public void onChange(final boolean selfChange) {
118            if (Constants.LOGVV) {
119                Log.v(Constants.TAG, "Service ContentObserver received notification");
120            }
121            updateFromProvider();
122        }
123
124    }
125
126    /**
127     * Gets called back when the connection to the media
128     * scanner is established or lost.
129     */
130    public class MediaScannerConnection implements ServiceConnection {
131        public void onServiceConnected(ComponentName className, IBinder service) {
132            if (Constants.LOGVV) {
133                Log.v(Constants.TAG, "Connected to Media Scanner");
134            }
135            synchronized (DownloadService.this) {
136                try {
137                    mMediaScannerConnecting = false;
138                    mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
139                    if (mMediaScannerService != null) {
140                        updateFromProvider();
141                    }
142                } finally {
143                    // notify anyone waiting on successful connection to MediaService
144                    DownloadService.this.notifyAll();
145                }
146            }
147        }
148
149        public void disconnectMediaScanner() {
150            synchronized (DownloadService.this) {
151                mMediaScannerConnecting = false;
152                if (mMediaScannerService != null) {
153                    mMediaScannerService = null;
154                    if (Constants.LOGVV) {
155                        Log.v(Constants.TAG, "Disconnecting from Media Scanner");
156                    }
157                    try {
158                        unbindService(this);
159                    } catch (IllegalArgumentException ex) {
160                        Log.w(Constants.TAG, "unbindService failed: " + ex);
161                    } finally {
162                        // notify anyone waiting on unsuccessful connection to MediaService
163                        DownloadService.this.notifyAll();
164                    }
165                }
166            }
167        }
168
169        public void onServiceDisconnected(ComponentName className) {
170            try {
171                if (Constants.LOGVV) {
172                    Log.v(Constants.TAG, "Disconnected from Media Scanner");
173                }
174            } finally {
175                synchronized (DownloadService.this) {
176                    mMediaScannerService = null;
177                    mMediaScannerConnecting = false;
178                    // notify anyone waiting on disconnect from MediaService
179                    DownloadService.this.notifyAll();
180                }
181            }
182        }
183    }
184
185    /**
186     * Returns an IBinder instance when someone wants to connect to this
187     * service. Binding to this service is not allowed.
188     *
189     * @throws UnsupportedOperationException
190     */
191    @Override
192    public IBinder onBind(Intent i) {
193        throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
194    }
195
196    /**
197     * Initializes the service when it is first created
198     */
199    @Override
200    public void onCreate() {
201        super.onCreate();
202        if (Constants.LOGVV) {
203            Log.v(Constants.TAG, "Service onCreate");
204        }
205
206        if (mSystemFacade == null) {
207            mSystemFacade = new RealSystemFacade(this);
208        }
209
210        mObserver = new DownloadManagerContentObserver();
211        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
212                true, mObserver);
213
214        mMediaScannerService = null;
215        mMediaScannerConnecting = false;
216        mMediaScannerConnection = new MediaScannerConnection();
217
218        mNotifier = new DownloadNotification(this, mSystemFacade);
219        mSystemFacade.cancelAllNotifications();
220
221        updateFromProvider();
222    }
223
224    @Override
225    public int onStartCommand(Intent intent, int flags, int startId) {
226        int returnValue = super.onStartCommand(intent, flags, startId);
227        if (Constants.LOGVV) {
228            Log.v(Constants.TAG, "Service onStart");
229        }
230        updateFromProvider();
231        return returnValue;
232    }
233
234    /**
235     * Cleans up when the service is destroyed
236     */
237    @Override
238    public void onDestroy() {
239        getContentResolver().unregisterContentObserver(mObserver);
240        if (Constants.LOGVV) {
241            Log.v(Constants.TAG, "Service onDestroy");
242        }
243        super.onDestroy();
244    }
245
246    /**
247     * Parses data from the content provider into private array
248     */
249    private void updateFromProvider() {
250        synchronized (this) {
251            mPendingUpdate = true;
252            if (mUpdateThread == null) {
253                mUpdateThread = new UpdateThread();
254                mSystemFacade.startThread(mUpdateThread);
255            }
256        }
257    }
258
259    private class UpdateThread extends Thread {
260        public UpdateThread() {
261            super("Download Service");
262        }
263
264        @Override
265        public void run() {
266            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
267
268            trimDatabase();
269            removeSpuriousFiles();
270
271            boolean keepService = false;
272            // for each update from the database, remember which download is
273            // supposed to get restarted soonest in the future
274            long wakeUp = Long.MAX_VALUE;
275            for (;;) {
276                synchronized (DownloadService.this) {
277                    if (mUpdateThread != this) {
278                        throw new IllegalStateException(
279                                "multiple UpdateThreads in DownloadService");
280                    }
281                    if (!mPendingUpdate) {
282                        mUpdateThread = null;
283                        if (!keepService) {
284                            stopSelf();
285                        }
286                        if (wakeUp != Long.MAX_VALUE) {
287                            scheduleAlarm(wakeUp);
288                        }
289                        return;
290                    }
291                    mPendingUpdate = false;
292                }
293
294                long now = mSystemFacade.currentTimeMillis();
295                boolean mustScan = false;
296                keepService = false;
297                wakeUp = Long.MAX_VALUE;
298                Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
299
300                Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
301                        null, null, null, null);
302                if (cursor == null) {
303                    continue;
304                }
305                try {
306                    DownloadInfo.Reader reader =
307                            new DownloadInfo.Reader(getContentResolver(), cursor);
308                    int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
309
310                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
311                        long id = cursor.getLong(idColumn);
312                        idsNoLongerInDatabase.remove(id);
313                        DownloadInfo info = mDownloads.get(id);
314                        if (info != null) {
315                            updateDownload(reader, info, now);
316                        } else {
317                            info = insertDownload(reader, now);
318                        }
319
320                        if (info.shouldScanFile() && !scanFile(info, true, false)) {
321                            mustScan = true;
322                            keepService = true;
323                        }
324                        if (info.hasCompletionNotification()) {
325                            keepService = true;
326                        }
327                        long next = info.nextAction(now);
328                        if (next == 0) {
329                            keepService = true;
330                        } else if (next > 0 && next < wakeUp) {
331                            wakeUp = next;
332                        }
333                    }
334                } finally {
335                    cursor.close();
336                }
337
338                for (Long id : idsNoLongerInDatabase) {
339                    deleteDownload(id);
340                }
341
342                // is there a need to start the DownloadService? yes, if there are rows to be
343                // deleted.
344                if (!mustScan) {
345                    for (DownloadInfo info : mDownloads.values()) {
346                        if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
347                            mustScan = true;
348                            keepService = true;
349                            break;
350                        }
351                    }
352                }
353                mNotifier.updateNotification(mDownloads.values());
354                if (mustScan) {
355                    bindMediaScanner();
356                } else {
357                    mMediaScannerConnection.disconnectMediaScanner();
358                }
359
360                // look for all rows with deleted flag set and delete the rows from the database
361                // permanently
362                for (DownloadInfo info : mDownloads.values()) {
363                    if (info.mDeleted) {
364                        // this row is to be deleted from the database. but does it have
365                        // mediaProviderUri?
366                        if (TextUtils.isEmpty(info.mMediaProviderUri)) {
367                            if (info.shouldScanFile()) {
368                                // initiate rescan of the file to - which will populate
369                                // mediaProviderUri column in this row
370                                if (!scanFile(info, true, false)) {
371                                    throw new IllegalStateException("scanFile failed!");
372                                }
373                            } else {
374                                // this file should NOT be scanned. delete the file.
375                                Helpers.deleteFile(getContentResolver(), info.mId, info.mFileName,
376                                        info.mMimeType);
377                            }
378                        } else {
379                            // yes it has mediaProviderUri column already filled in.
380                            // delete it from MediaProvider database and then from downloads table
381                            // in DownProvider database (the order of deletion is important).
382                            getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
383                                    null);
384                            getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
385                                    Downloads.Impl._ID + " = ? ",
386                                    new String[]{String.valueOf(info.mId)});
387                        }
388                    }
389                }
390            }
391        }
392
393        private void bindMediaScanner() {
394            if (!mMediaScannerConnecting) {
395                Intent intent = new Intent();
396                intent.setClassName("com.android.providers.media",
397                        "com.android.providers.media.MediaScannerService");
398                mMediaScannerConnecting = true;
399                bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
400            }
401        }
402
403        private void scheduleAlarm(long wakeUp) {
404            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
405            if (alarms == null) {
406                Log.e(Constants.TAG, "couldn't get alarm manager");
407                return;
408            }
409
410            if (Constants.LOGV) {
411                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
412            }
413
414            Intent intent = new Intent(Constants.ACTION_RETRY);
415            intent.setClassName("com.android.providers.downloads",
416                    DownloadReceiver.class.getName());
417            alarms.set(
418                    AlarmManager.RTC_WAKEUP,
419                    mSystemFacade.currentTimeMillis() + wakeUp,
420                    PendingIntent.getBroadcast(DownloadService.this, 0, intent,
421                            PendingIntent.FLAG_ONE_SHOT));
422        }
423    }
424
425    /**
426     * Removes files that may have been left behind in the cache directory
427     */
428    private void removeSpuriousFiles() {
429        File[] files = Environment.getDownloadCacheDirectory().listFiles();
430        if (files == null) {
431            // The cache folder doesn't appear to exist (this is likely the case
432            // when running the simulator).
433            return;
434        }
435        HashSet<String> fileSet = new HashSet<String>();
436        for (int i = 0; i < files.length; i++) {
437            if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
438                continue;
439            }
440            if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
441                continue;
442            }
443            fileSet.add(files[i].getPath());
444        }
445
446        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
447                new String[] { Downloads.Impl._DATA }, null, null, null);
448        if (cursor != null) {
449            if (cursor.moveToFirst()) {
450                do {
451                    fileSet.remove(cursor.getString(0));
452                } while (cursor.moveToNext());
453            }
454            cursor.close();
455        }
456        Iterator<String> iterator = fileSet.iterator();
457        while (iterator.hasNext()) {
458            String filename = iterator.next();
459            if (Constants.LOGV) {
460                Log.v(Constants.TAG, "deleting spurious file " + filename);
461            }
462            new File(filename).delete();
463        }
464    }
465
466    /**
467     * Drops old rows from the database to prevent it from growing too large
468     */
469    private void trimDatabase() {
470        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
471                new String[] { Downloads.Impl._ID },
472                Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
473                Downloads.Impl.COLUMN_LAST_MODIFICATION);
474        if (cursor == null) {
475            // This isn't good - if we can't do basic queries in our database, nothing's gonna work
476            Log.e(Constants.TAG, "null cursor in trimDatabase");
477            return;
478        }
479        if (cursor.moveToFirst()) {
480            int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
481            int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
482            while (numDelete > 0) {
483                Uri downloadUri = ContentUris.withAppendedId(
484                        Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
485                getContentResolver().delete(downloadUri, null, null);
486                if (!cursor.moveToNext()) {
487                    break;
488                }
489                numDelete--;
490            }
491        }
492        cursor.close();
493    }
494
495    /**
496     * Keeps a local copy of the info about a download, and initiates the
497     * download if appropriate.
498     */
499    private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
500        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
501        mDownloads.put(info.mId, info);
502
503        if (Constants.LOGVV) {
504            info.logVerboseInfo();
505        }
506
507        info.startIfReady(now);
508        return info;
509    }
510
511    /**
512     * Updates the local copy of the info about a download.
513     */
514    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
515        int oldVisibility = info.mVisibility;
516        int oldStatus = info.mStatus;
517
518        reader.updateFromDatabase(info);
519
520        boolean lostVisibility =
521                oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
522                && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
523                && Downloads.Impl.isStatusCompleted(info.mStatus);
524        boolean justCompleted =
525                !Downloads.Impl.isStatusCompleted(oldStatus)
526                && Downloads.Impl.isStatusCompleted(info.mStatus);
527        if (lostVisibility || justCompleted) {
528            mSystemFacade.cancelNotification(info.mId);
529        }
530
531        info.startIfReady(now);
532    }
533
534    /**
535     * Removes the local copy of the info about a download.
536     */
537    private void deleteDownload(long id) {
538        DownloadInfo info = mDownloads.get(id);
539        if (info.shouldScanFile()) {
540            scanFile(info, false, false);
541        }
542        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
543            info.mStatus = Downloads.Impl.STATUS_CANCELED;
544        }
545        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
546            new File(info.mFileName).delete();
547        }
548        mSystemFacade.cancelNotification(info.mId);
549        mDownloads.remove(info.mId);
550    }
551
552    /**
553     * Attempts to scan the file if necessary.
554     * @return true if the file has been properly scanned.
555     */
556    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
557            final boolean deleteFile) {
558        synchronized (this) {
559            if (mMediaScannerService == null) {
560                // not bound to mediaservice. but if in the process of connecting to it, wait until
561                // connection is resolved
562                while (mMediaScannerConnecting) {
563                    Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
564                    try {
565                        this.wait(WAIT_TIMEOUT);
566                    } catch (InterruptedException e1) {
567                        throw new IllegalStateException("wait interrupted");
568                    }
569                }
570            }
571            // do we have mediaservice?
572            if (mMediaScannerService == null) {
573                // no available MediaService And not even in the process of connecting to it
574                return false;
575            }
576            if (Constants.LOGV) {
577                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
578            }
579            try {
580                final Uri key = info.getAllDownloadsUri();
581                final String mimeType = info.mMimeType;
582                final ContentResolver resolver = getContentResolver();
583                final long id = info.mId;
584                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
585                        new IMediaScannerListener.Stub() {
586                            public void scanCompleted(String path, Uri uri) {
587                                if (uri != null && updateDatabase) {
588                                    // file is scanned and mediaprovider returned uri. store it in downloads
589                                    // table (i.e., update this downloaded file's row)
590                                    ContentValues values = new ContentValues();
591                                    values.put(Constants.MEDIA_SCANNED, 1);
592                                    values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
593                                            uri.toString());
594                                    getContentResolver().update(key, values, null, null);
595                                } else if (uri == null && deleteFile) {
596                                    // callback returned NO uri..that means this file doesn't
597                                    // exist in MediaProvider. but it still needs to be deleted
598                                    // TODO don't scan files that are not scannable by MediaScanner.
599                                    //      create a public method in MediaFile.java to return false
600                                    //      if the given file's mimetype is not any of the types
601                                    //      the mediaprovider is interested in.
602                                    Helpers.deleteFile(resolver, id, path, mimeType);
603                                }
604                            }
605                        });
606                return true;
607            } catch (RemoteException e) {
608                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
609                return false;
610            }
611        }
612    }
613}
614