DownloadService.java revision 3ff0baf4ed8eaba1b21979335ff1b9d8b2fede70
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                    if (Constants.LOGVV) {
305                        Log.i(Constants.TAG, "number of rows from downloads-db: " +
306                                cursor.getCount());
307                    }
308                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
309                        long id = cursor.getLong(idColumn);
310                        idsNoLongerInDatabase.remove(id);
311                        DownloadInfo info = mDownloads.get(id);
312                        if (info != null) {
313                            updateDownload(reader, info, now);
314                        } else {
315                            info = insertDownload(reader, now);
316                        }
317
318                        if (info.shouldScanFile() && !scanFile(info, true, false)) {
319                            mustScan = true;
320                            keepService = true;
321                        }
322                        if (info.hasCompletionNotification()) {
323                            keepService = true;
324                        }
325                        long next = info.nextAction(now);
326                        if (next == 0) {
327                            keepService = true;
328                        } else if (next > 0 && next < wakeUp) {
329                            wakeUp = next;
330                        }
331                    }
332                } finally {
333                    cursor.close();
334                }
335
336                for (Long id : idsNoLongerInDatabase) {
337                    deleteDownload(id);
338                }
339
340                // is there a need to start the DownloadService? yes, if there are rows to be
341                // deleted.
342                if (!mustScan) {
343                    for (DownloadInfo info : mDownloads.values()) {
344                        if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
345                            mustScan = true;
346                            keepService = true;
347                            break;
348                        }
349                    }
350                }
351                mNotifier.updateNotification(mDownloads.values());
352                if (mustScan) {
353                    bindMediaScanner();
354                } else {
355                    mMediaScannerConnection.disconnectMediaScanner();
356                }
357
358                // look for all rows with deleted flag set and delete the rows from the database
359                // permanently
360                for (DownloadInfo info : mDownloads.values()) {
361                    if (info.mDeleted) {
362                        // this row is to be deleted from the database. but does it have
363                        // mediaProviderUri?
364                        if (TextUtils.isEmpty(info.mMediaProviderUri)) {
365                            if (info.shouldScanFile()) {
366                                // initiate rescan of the file to - which will populate
367                                // mediaProviderUri column in this row
368                                if (!scanFile(info, false, true)) {
369                                    throw new IllegalStateException("scanFile failed!");
370                                }
371                                continue;
372                            }
373                        } else {
374                            // yes it has mediaProviderUri column already filled in.
375                            // delete it from MediaProvider database.
376                            getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
377                                    null);
378                        }
379                        // delete the file
380                        deleteFileIfExists(info.mFileName);
381                        // delete from the downloads db
382                        getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
383                                Downloads.Impl._ID + " = ? ",
384                                new String[]{String.valueOf(info.mId)});
385                    }
386                }
387            }
388        }
389
390        private void bindMediaScanner() {
391            if (!mMediaScannerConnecting) {
392                Intent intent = new Intent();
393                intent.setClassName("com.android.providers.media",
394                        "com.android.providers.media.MediaScannerService");
395                mMediaScannerConnecting = true;
396                bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
397            }
398        }
399
400        private void scheduleAlarm(long wakeUp) {
401            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
402            if (alarms == null) {
403                Log.e(Constants.TAG, "couldn't get alarm manager");
404                return;
405            }
406
407            if (Constants.LOGV) {
408                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
409            }
410
411            Intent intent = new Intent(Constants.ACTION_RETRY);
412            intent.setClassName("com.android.providers.downloads",
413                    DownloadReceiver.class.getName());
414            alarms.set(
415                    AlarmManager.RTC_WAKEUP,
416                    mSystemFacade.currentTimeMillis() + wakeUp,
417                    PendingIntent.getBroadcast(DownloadService.this, 0, intent,
418                            PendingIntent.FLAG_ONE_SHOT));
419        }
420    }
421
422    /**
423     * Keeps a local copy of the info about a download, and initiates the
424     * download if appropriate.
425     */
426    private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
427        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
428        mDownloads.put(info.mId, info);
429
430        if (Constants.LOGVV) {
431            Log.v(Constants.TAG, "processing inserted download " + info.mId);
432        }
433
434        info.startIfReady(now, mStorageManager);
435        return info;
436    }
437
438    /**
439     * Updates the local copy of the info about a download.
440     */
441    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
442        int oldVisibility = info.mVisibility;
443        int oldStatus = info.mStatus;
444
445        reader.updateFromDatabase(info);
446        if (Constants.LOGVV) {
447            Log.v(Constants.TAG, "processing updated download " + info.mId +
448                    ", status: " + info.mStatus);
449        }
450
451        boolean lostVisibility =
452                oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
453                && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
454                && Downloads.Impl.isStatusCompleted(info.mStatus);
455        boolean justCompleted =
456                !Downloads.Impl.isStatusCompleted(oldStatus)
457                && Downloads.Impl.isStatusCompleted(info.mStatus);
458        if (lostVisibility || justCompleted) {
459            mSystemFacade.cancelNotification(info.mId);
460        }
461
462        info.startIfReady(now, mStorageManager);
463    }
464
465    /**
466     * Removes the local copy of the info about a download.
467     */
468    private void deleteDownload(long id) {
469        DownloadInfo info = mDownloads.get(id);
470        if (info.shouldScanFile()) {
471            scanFile(info, false, false);
472        }
473        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
474            info.mStatus = Downloads.Impl.STATUS_CANCELED;
475        }
476        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
477            new File(info.mFileName).delete();
478        }
479        mSystemFacade.cancelNotification(info.mId);
480        mDownloads.remove(info.mId);
481    }
482
483    /**
484     * Attempts to scan the file if necessary.
485     * @return true if the file has been properly scanned.
486     */
487    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
488            final boolean deleteFile) {
489        synchronized (this) {
490            if (mMediaScannerService == null) {
491                // not bound to mediaservice. but if in the process of connecting to it, wait until
492                // connection is resolved
493                while (mMediaScannerConnecting) {
494                    Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
495                    try {
496                        this.wait(WAIT_TIMEOUT);
497                    } catch (InterruptedException e1) {
498                        throw new IllegalStateException("wait interrupted");
499                    }
500                }
501            }
502            // do we have mediaservice?
503            if (mMediaScannerService == null) {
504                // no available MediaService And not even in the process of connecting to it
505                return false;
506            }
507            if (Constants.LOGV) {
508                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
509            }
510            try {
511                final Uri key = info.getAllDownloadsUri();
512                final long id = info.mId;
513                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
514                        new IMediaScannerListener.Stub() {
515                            public void scanCompleted(String path, Uri uri) {
516                                if (updateDatabase) {
517                                    // Mark this as 'scanned' in the database
518                                    // so that it is NOT subject to re-scanning by MediaScanner
519                                    // next time this database row row is encountered
520                                    ContentValues values = new ContentValues();
521                                    values.put(Constants.MEDIA_SCANNED, 1);
522                                    if (uri != null) {
523                                        values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
524                                                uri.toString());
525                                    }
526                                    getContentResolver().update(key, values, null, null);
527                                } else if (deleteFile) {
528                                    if (uri != null) {
529                                        // use the Uri returned to delete it from the MediaProvider
530                                        getContentResolver().delete(uri, null, null);
531                                    }
532                                    // delete the file and delete its row from the downloads db
533                                    deleteFileIfExists(path);
534                                    getContentResolver().delete(
535                                            Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
536                                            Downloads.Impl._ID + " = ? ",
537                                            new String[]{String.valueOf(id)});
538                                }
539                            }
540                        });
541                return true;
542            } catch (RemoteException e) {
543                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
544                return false;
545            }
546        }
547    }
548
549    private void deleteFileIfExists(String path) {
550        try {
551            if (!TextUtils.isEmpty(path)) {
552                File file = new File(path);
553                file.delete();
554            }
555        } catch (Exception e) {
556            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
557        }
558    }
559}
560