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