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