DownloadService.java revision 5218d33d57990c3e3549c58bd3f0ac244dfc3d59
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.downloads;
18
19import com.google.android.collect.Maps;
20import com.google.common.annotations.VisibleForTesting;
21
22import android.app.AlarmManager;
23import android.app.PendingIntent;
24import android.app.Service;
25import android.content.ComponentName;
26import android.content.ContentResolver;
27import android.content.ContentValues;
28import android.content.Context;
29import android.content.Intent;
30import android.content.ServiceConnection;
31import android.database.ContentObserver;
32import android.database.Cursor;
33import android.media.IMediaScannerListener;
34import android.media.IMediaScannerService;
35import android.net.Uri;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.Process;
39import android.os.RemoteException;
40import android.provider.Downloads;
41import android.text.TextUtils;
42import android.util.Log;
43
44import java.io.File;
45import java.util.HashSet;
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    /** amount of time to wait to connect to MediaScannerService before timing out */
55    private static final long WAIT_TIMEOUT = 10 * 1000;
56
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    private StorageManager mStorageManager;
101
102    /**
103     * Receives notifications when the data in the content provider changes
104     */
105    private class DownloadManagerContentObserver extends ContentObserver {
106
107        public DownloadManagerContentObserver() {
108            super(new Handler());
109        }
110
111        /**
112         * Receives notification when the data in the observed content
113         * provider changes.
114         */
115        @Override
116        public void onChange(final boolean selfChange) {
117            if (Constants.LOGVV) {
118                Log.v(Constants.TAG, "Service ContentObserver received notification");
119            }
120            updateFromProvider();
121        }
122
123    }
124
125    /**
126     * Gets called back when the connection to the media
127     * scanner is established or lost.
128     */
129    public class MediaScannerConnection implements ServiceConnection {
130        public void onServiceConnected(ComponentName className, IBinder service) {
131            if (Constants.LOGVV) {
132                Log.v(Constants.TAG, "Connected to Media Scanner");
133            }
134            synchronized (DownloadService.this) {
135                try {
136                    mMediaScannerConnecting = false;
137                    mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
138                    if (mMediaScannerService != null) {
139                        updateFromProvider();
140                    }
141                } finally {
142                    // notify anyone waiting on successful connection to MediaService
143                    DownloadService.this.notifyAll();
144                }
145            }
146        }
147
148        public void disconnectMediaScanner() {
149            synchronized (DownloadService.this) {
150                mMediaScannerConnecting = false;
151                if (mMediaScannerService != null) {
152                    mMediaScannerService = null;
153                    if (Constants.LOGVV) {
154                        Log.v(Constants.TAG, "Disconnecting from Media Scanner");
155                    }
156                    try {
157                        unbindService(this);
158                    } catch (IllegalArgumentException ex) {
159                        Log.w(Constants.TAG, "unbindService failed: " + ex);
160                    } finally {
161                        // notify anyone waiting on unsuccessful connection to MediaService
162                        DownloadService.this.notifyAll();
163                    }
164                }
165            }
166        }
167
168        public void onServiceDisconnected(ComponentName className) {
169            try {
170                if (Constants.LOGVV) {
171                    Log.v(Constants.TAG, "Disconnected from Media Scanner");
172                }
173            } finally {
174                synchronized (DownloadService.this) {
175                    mMediaScannerService = null;
176                    mMediaScannerConnecting = false;
177                    // notify anyone waiting on disconnect from MediaService
178                    DownloadService.this.notifyAll();
179                }
180            }
181        }
182    }
183
184    /**
185     * Returns an IBinder instance when someone wants to connect to this
186     * service. Binding to this service is not allowed.
187     *
188     * @throws UnsupportedOperationException
189     */
190    @Override
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    @Override
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        mStorageManager = StorageManager.getInstance(getApplicationContext());
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    @Override
237    public void onDestroy() {
238        getContentResolver().unregisterContentObserver(mObserver);
239        if (Constants.LOGVV) {
240            Log.v(Constants.TAG, "Service onDestroy");
241        }
242        super.onDestroy();
243    }
244
245    /**
246     * Parses data from the content provider into private array
247     */
248    private void updateFromProvider() {
249        synchronized (this) {
250            mPendingUpdate = true;
251            if (mUpdateThread == null) {
252                mUpdateThread = new UpdateThread();
253                mSystemFacade.startThread(mUpdateThread);
254            }
255        }
256    }
257
258    private class UpdateThread extends Thread {
259        public UpdateThread() {
260            super("Download Service");
261        }
262
263        @Override
264        public void run() {
265            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
266            boolean keepService = false;
267            // for each update from the database, remember which download is
268            // supposed to get restarted soonest in the future
269            long wakeUp = Long.MAX_VALUE;
270            for (;;) {
271                synchronized (DownloadService.this) {
272                    if (mUpdateThread != this) {
273                        throw new IllegalStateException(
274                                "multiple UpdateThreads in DownloadService");
275                    }
276                    if (!mPendingUpdate) {
277                        mUpdateThread = null;
278                        if (!keepService) {
279                            stopSelf();
280                        }
281                        if (wakeUp != Long.MAX_VALUE) {
282                            scheduleAlarm(wakeUp);
283                        }
284                        return;
285                    }
286                    mPendingUpdate = false;
287                }
288
289                long now = mSystemFacade.currentTimeMillis();
290                boolean mustScan = false;
291                keepService = false;
292                wakeUp = Long.MAX_VALUE;
293                Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
294
295                Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
296                        null, null, null, null);
297                if (cursor == null) {
298                    continue;
299                }
300                try {
301                    DownloadInfo.Reader reader =
302                            new DownloadInfo.Reader(getContentResolver(), cursor);
303                    int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
304
305                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
306                        long id = cursor.getLong(idColumn);
307                        idsNoLongerInDatabase.remove(id);
308                        DownloadInfo info = mDownloads.get(id);
309                        if (info != null) {
310                            updateDownload(reader, info, now);
311                        } else {
312                            info = insertDownload(reader, now);
313                        }
314
315                        if (info.shouldScanFile() && !scanFile(info, true, false)) {
316                            mustScan = true;
317                            keepService = true;
318                        }
319                        if (info.hasCompletionNotification()) {
320                            keepService = true;
321                        }
322                        long next = info.nextAction(now);
323                        if (next == 0) {
324                            keepService = true;
325                        } else if (next > 0 && next < wakeUp) {
326                            wakeUp = next;
327                        }
328                    }
329                } finally {
330                    cursor.close();
331                }
332
333                for (Long id : idsNoLongerInDatabase) {
334                    deleteDownload(id);
335                }
336
337                // is there a need to start the DownloadService? yes, if there are rows to be
338                // deleted.
339                if (!mustScan) {
340                    for (DownloadInfo info : mDownloads.values()) {
341                        if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
342                            mustScan = true;
343                            keepService = true;
344                            break;
345                        }
346                    }
347                }
348                mNotifier.updateNotification(mDownloads.values());
349                if (mustScan) {
350                    bindMediaScanner();
351                } else {
352                    mMediaScannerConnection.disconnectMediaScanner();
353                }
354
355                // look for all rows with deleted flag set and delete the rows from the database
356                // permanently
357                for (DownloadInfo info : mDownloads.values()) {
358                    if (info.mDeleted) {
359                        // this row is to be deleted from the database. but does it have
360                        // mediaProviderUri?
361                        if (TextUtils.isEmpty(info.mMediaProviderUri)) {
362                            if (info.shouldScanFile()) {
363                                // initiate rescan of the file to - which will populate
364                                // mediaProviderUri column in this row
365                                if (!scanFile(info, false, true)) {
366                                    throw new IllegalStateException("scanFile failed!");
367                                }
368                                continue;
369                            }
370                        } else {
371                            // yes it has mediaProviderUri column already filled in.
372                            // delete it from MediaProvider database.
373                            getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
374                                    null);
375                        }
376                        // delete the file
377                        deleteFileIfExists(info.mFileName);
378                        // delete from the downloads db
379                        getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
380                                Downloads.Impl._ID + " = ? ",
381                                new String[]{String.valueOf(info.mId)});
382                    }
383                }
384            }
385        }
386
387        private void bindMediaScanner() {
388            if (!mMediaScannerConnecting) {
389                Intent intent = new Intent();
390                intent.setClassName("com.android.providers.media",
391                        "com.android.providers.media.MediaScannerService");
392                mMediaScannerConnecting = true;
393                bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
394            }
395        }
396
397        private void scheduleAlarm(long wakeUp) {
398            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
399            if (alarms == null) {
400                Log.e(Constants.TAG, "couldn't get alarm manager");
401                return;
402            }
403
404            if (Constants.LOGV) {
405                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
406            }
407
408            Intent intent = new Intent(Constants.ACTION_RETRY);
409            intent.setClassName("com.android.providers.downloads",
410                    DownloadReceiver.class.getName());
411            alarms.set(
412                    AlarmManager.RTC_WAKEUP,
413                    mSystemFacade.currentTimeMillis() + wakeUp,
414                    PendingIntent.getBroadcast(DownloadService.this, 0, intent,
415                            PendingIntent.FLAG_ONE_SHOT));
416        }
417    }
418
419    /**
420     * Keeps a local copy of the info about a download, and initiates the
421     * download if appropriate.
422     */
423    private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
424        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
425        mDownloads.put(info.mId, info);
426
427        if (Constants.LOGVV) {
428            info.logVerboseInfo();
429        }
430
431        info.startIfReady(now, mStorageManager);
432        return info;
433    }
434
435    /**
436     * Updates the local copy of the info about a download.
437     */
438    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
439        int oldVisibility = info.mVisibility;
440        int oldStatus = info.mStatus;
441
442        reader.updateFromDatabase(info);
443
444        boolean lostVisibility =
445                oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
446                && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
447                && Downloads.Impl.isStatusCompleted(info.mStatus);
448        boolean justCompleted =
449                !Downloads.Impl.isStatusCompleted(oldStatus)
450                && Downloads.Impl.isStatusCompleted(info.mStatus);
451        if (lostVisibility || justCompleted) {
452            mSystemFacade.cancelNotification(info.mId);
453        }
454
455        info.startIfReady(now, mStorageManager);
456    }
457
458    /**
459     * Removes the local copy of the info about a download.
460     */
461    private void deleteDownload(long id) {
462        DownloadInfo info = mDownloads.get(id);
463        if (info.shouldScanFile()) {
464            scanFile(info, false, false);
465        }
466        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
467            info.mStatus = Downloads.Impl.STATUS_CANCELED;
468        }
469        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
470            new File(info.mFileName).delete();
471        }
472        mSystemFacade.cancelNotification(info.mId);
473        mDownloads.remove(info.mId);
474    }
475
476    /**
477     * Attempts to scan the file if necessary.
478     * @return true if the file has been properly scanned.
479     */
480    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
481            final boolean deleteFile) {
482        synchronized (this) {
483            if (mMediaScannerService == null) {
484                // not bound to mediaservice. but if in the process of connecting to it, wait until
485                // connection is resolved
486                while (mMediaScannerConnecting) {
487                    Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
488                    try {
489                        this.wait(WAIT_TIMEOUT);
490                    } catch (InterruptedException e1) {
491                        throw new IllegalStateException("wait interrupted");
492                    }
493                }
494            }
495            // do we have mediaservice?
496            if (mMediaScannerService == null) {
497                // no available MediaService And not even in the process of connecting to it
498                return false;
499            }
500            if (Constants.LOGV) {
501                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
502            }
503            try {
504                final Uri key = info.getAllDownloadsUri();
505                final String mimeType = info.mMimeType;
506                final ContentResolver resolver = getContentResolver();
507                final long id = info.mId;
508                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
509                        new IMediaScannerListener.Stub() {
510                            public void scanCompleted(String path, Uri uri) {
511                                if (updateDatabase) {
512                                    // Mark this as 'scanned' in the database
513                                    // so that it is NOT subject to re-scanning by MediaScanner
514                                    // next time this database row row is encountered
515                                    ContentValues values = new ContentValues();
516                                    values.put(Constants.MEDIA_SCANNED, 1);
517                                    if (uri != null) {
518                                        values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
519                                                uri.toString());
520                                    }
521                                    getContentResolver().update(key, values, null, null);
522                                } else if (deleteFile) {
523                                    if (uri != null) {
524                                        // use the Uri returned to delete it from the MediaProvider
525                                        getContentResolver().delete(uri, null, null);
526                                    }
527                                    // delete the file and delete its row from the downloads db
528                                    deleteFileIfExists(path);
529                                    getContentResolver().delete(
530                                            Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
531                                            Downloads.Impl._ID + " = ? ",
532                                            new String[]{String.valueOf(id)});
533                                }
534                            }
535                        });
536                return true;
537            } catch (RemoteException e) {
538                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
539                return false;
540            }
541        }
542    }
543
544    private void deleteFileIfExists(String path) {
545        try {
546            if (!TextUtils.isEmpty(path)) {
547                File file = new File(path);
548                file.delete();
549            }
550        } catch (Exception e) {
551            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
552        }
553    }
554}
555