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