DownloadService.java revision ebcc93801eb9545004536587d10f82db378aeab5
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 static com.android.providers.downloads.Constants.TAG;
20
21import android.app.AlarmManager;
22import android.app.PendingIntent;
23import android.app.Service;
24import android.content.ComponentName;
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.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.android.internal.util.IndentingPrintWriter;
43import com.google.android.collect.Maps;
44import com.google.common.annotations.VisibleForTesting;
45import com.google.common.collect.Lists;
46
47import java.io.File;
48import java.io.FileDescriptor;
49import java.io.PrintWriter;
50import java.util.Collections;
51import java.util.HashSet;
52import java.util.List;
53import java.util.Map;
54import java.util.Set;
55
56/**
57 * Performs the background downloads requested by applications that use the Downloads provider.
58 */
59public class DownloadService extends Service {
60    /** amount of time to wait to connect to MediaScannerService before timing out */
61    private static final long WAIT_TIMEOUT = 10 * 1000;
62
63    /** Observer to get notified when the content observer's data changes */
64    private DownloadManagerContentObserver mObserver;
65
66    /** Class to handle Notification Manager updates */
67    private DownloadNotification mNotifier;
68
69    /**
70     * The Service's view of the list of downloads, mapping download IDs to the corresponding info
71     * object. This is kept independently from the content provider, and the Service only initiates
72     * downloads based on this data, so that it can deal with situation where the data in the
73     * content provider changes or disappears.
74     */
75    private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
76
77    /**
78     * The thread that updates the internal download list from the content
79     * provider.
80     */
81    @VisibleForTesting
82    UpdateThread mUpdateThread;
83
84    /**
85     * Whether the internal download list should be updated from the content
86     * provider.
87     */
88    private boolean mPendingUpdate;
89
90    /**
91     * The ServiceConnection object that tells us when we're connected to and disconnected from
92     * the Media Scanner
93     */
94    private MediaScannerConnection mMediaScannerConnection;
95
96    private boolean mMediaScannerConnecting;
97
98    /**
99     * The IPC interface to the Media Scanner
100     */
101    private IMediaScannerService mMediaScannerService;
102
103    @VisibleForTesting
104    SystemFacade mSystemFacade;
105
106    private StorageManager mStorageManager;
107
108    /**
109     * Receives notifications when the data in the content provider changes
110     */
111    private class DownloadManagerContentObserver extends ContentObserver {
112
113        public DownloadManagerContentObserver() {
114            super(new Handler());
115        }
116
117        /**
118         * Receives notification when the data in the observed content
119         * provider changes.
120         */
121        @Override
122        public void onChange(final boolean selfChange) {
123            if (Constants.LOGVV) {
124                Log.v(Constants.TAG, "Service ContentObserver received notification");
125            }
126            updateFromProvider();
127        }
128
129    }
130
131    /**
132     * Gets called back when the connection to the media
133     * scanner is established or lost.
134     */
135    public class MediaScannerConnection implements ServiceConnection {
136        public void onServiceConnected(ComponentName className, IBinder service) {
137            if (Constants.LOGVV) {
138                Log.v(Constants.TAG, "Connected to Media Scanner");
139            }
140            synchronized (DownloadService.this) {
141                try {
142                    mMediaScannerConnecting = false;
143                    mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
144                    if (mMediaScannerService != null) {
145                        updateFromProvider();
146                    }
147                } finally {
148                    // notify anyone waiting on successful connection to MediaService
149                    DownloadService.this.notifyAll();
150                }
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                    } finally {
167                        // notify anyone waiting on unsuccessful connection to MediaService
168                        DownloadService.this.notifyAll();
169                    }
170                }
171            }
172        }
173
174        public void onServiceDisconnected(ComponentName className) {
175            try {
176                if (Constants.LOGVV) {
177                    Log.v(Constants.TAG, "Disconnected from Media Scanner");
178                }
179            } finally {
180                synchronized (DownloadService.this) {
181                    mMediaScannerService = null;
182                    mMediaScannerConnecting = false;
183                    // notify anyone waiting on disconnect from MediaService
184                    DownloadService.this.notifyAll();
185                }
186            }
187        }
188    }
189
190    /**
191     * Returns an IBinder instance when someone wants to connect to this
192     * service. Binding to this service is not allowed.
193     *
194     * @throws UnsupportedOperationException
195     */
196    @Override
197    public IBinder onBind(Intent i) {
198        throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
199    }
200
201    /**
202     * Initializes the service when it is first created
203     */
204    @Override
205    public void onCreate() {
206        super.onCreate();
207        if (Constants.LOGVV) {
208            Log.v(Constants.TAG, "Service onCreate");
209        }
210
211        if (mSystemFacade == null) {
212            mSystemFacade = new RealSystemFacade(this);
213        }
214
215        mObserver = new DownloadManagerContentObserver();
216        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
217                true, mObserver);
218
219        mMediaScannerService = null;
220        mMediaScannerConnecting = false;
221        mMediaScannerConnection = new MediaScannerConnection();
222
223        mNotifier = new DownloadNotification(this, mSystemFacade);
224        mSystemFacade.cancelAllNotifications();
225        mStorageManager = StorageManager.getInstance(getApplicationContext());
226        updateFromProvider();
227    }
228
229    @Override
230    public int onStartCommand(Intent intent, int flags, int startId) {
231        int returnValue = super.onStartCommand(intent, flags, startId);
232        if (Constants.LOGVV) {
233            Log.v(Constants.TAG, "Service onStart");
234        }
235        updateFromProvider();
236        return returnValue;
237    }
238
239    /**
240     * Cleans up when the service is destroyed
241     */
242    @Override
243    public void onDestroy() {
244        getContentResolver().unregisterContentObserver(mObserver);
245        if (Constants.LOGVV) {
246            Log.v(Constants.TAG, "Service onDestroy");
247        }
248        super.onDestroy();
249    }
250
251    /**
252     * Parses data from the content provider into private array
253     */
254    private void updateFromProvider() {
255        synchronized (this) {
256            mPendingUpdate = true;
257            if (mUpdateThread == null) {
258                mUpdateThread = new UpdateThread();
259                mSystemFacade.startThread(mUpdateThread);
260            }
261        }
262    }
263
264    private class UpdateThread extends Thread {
265        public UpdateThread() {
266            super("Download Service");
267        }
268
269        @Override
270        public void run() {
271            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
272            boolean keepService = false;
273            // for each update from the database, remember which download is
274            // supposed to get restarted soonest in the future
275            long wakeUp = Long.MAX_VALUE;
276            for (;;) {
277                synchronized (DownloadService.this) {
278                    if (mUpdateThread != this) {
279                        throw new IllegalStateException(
280                                "multiple UpdateThreads in DownloadService");
281                    }
282                    if (!mPendingUpdate) {
283                        mUpdateThread = null;
284                        if (!keepService) {
285                            stopSelf();
286                        }
287                        if (wakeUp != Long.MAX_VALUE) {
288                            scheduleAlarm(wakeUp);
289                        }
290                        return;
291                    }
292                    mPendingUpdate = false;
293                }
294
295                synchronized (mDownloads) {
296                    long now = mSystemFacade.currentTimeMillis();
297                    boolean mustScan = false;
298                    keepService = false;
299                    wakeUp = Long.MAX_VALUE;
300                    Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
301
302                    Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
303                            null, null, null, null);
304                    if (cursor == null) {
305                        continue;
306                    }
307                    try {
308                        DownloadInfo.Reader reader =
309                                new DownloadInfo.Reader(getContentResolver(), cursor);
310                        int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
311                        if (Constants.LOGVV) {
312                            Log.i(Constants.TAG, "number of rows from downloads-db: " +
313                                    cursor.getCount());
314                        }
315                        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
316                            long id = cursor.getLong(idColumn);
317                            idsNoLongerInDatabase.remove(id);
318                            DownloadInfo info = mDownloads.get(id);
319                            if (info != null) {
320                                updateDownload(reader, info, now);
321                            } else {
322                                info = insertDownloadLocked(reader, now);
323                            }
324
325                            if (info.shouldScanFile() && !scanFile(info, true, false)) {
326                                mustScan = true;
327                                keepService = true;
328                            }
329                            if (info.hasCompletionNotification()) {
330                                keepService = true;
331                            }
332                            long next = info.nextAction(now);
333                            if (next == 0) {
334                                keepService = true;
335                            } else if (next > 0 && next < wakeUp) {
336                                wakeUp = next;
337                            }
338                        }
339                    } finally {
340                        cursor.close();
341                    }
342
343                    for (Long id : idsNoLongerInDatabase) {
344                        deleteDownloadLocked(id);
345                    }
346
347                    // is there a need to start the DownloadService? yes, if there are rows to be
348                    // deleted.
349                    if (!mustScan) {
350                        for (DownloadInfo info : mDownloads.values()) {
351                            if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
352                                mustScan = true;
353                                keepService = true;
354                                break;
355                            }
356                        }
357                    }
358                    mNotifier.updateNotification(mDownloads.values());
359                    if (mustScan) {
360                        bindMediaScanner();
361                    } else {
362                        mMediaScannerConnection.disconnectMediaScanner();
363                    }
364
365                    // look for all rows with deleted flag set and delete the rows from the database
366                    // permanently
367                    for (DownloadInfo info : mDownloads.values()) {
368                        if (info.mDeleted) {
369                            // this row is to be deleted from the database. but does it have
370                            // mediaProviderUri?
371                            if (TextUtils.isEmpty(info.mMediaProviderUri)) {
372                                if (info.shouldScanFile()) {
373                                    // initiate rescan of the file to - which will populate
374                                    // mediaProviderUri column in this row
375                                    if (!scanFile(info, false, true)) {
376                                        throw new IllegalStateException("scanFile failed!");
377                                    }
378                                    continue;
379                                }
380                            } else {
381                                // yes it has mediaProviderUri column already filled in.
382                                // delete it from MediaProvider database.
383                                getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
384                                        null);
385                            }
386                            // delete the file
387                            deleteFileIfExists(info.mFileName);
388                            // delete from the downloads db
389                            getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
390                                    Downloads.Impl._ID + " = ? ",
391                                    new String[]{String.valueOf(info.mId)});
392                        }
393                    }
394                }
395            }
396        }
397
398        private void bindMediaScanner() {
399            if (!mMediaScannerConnecting) {
400                Intent intent = new Intent();
401                intent.setClassName("com.android.providers.media",
402                        "com.android.providers.media.MediaScannerService");
403                mMediaScannerConnecting = true;
404                bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
405            }
406        }
407
408        private void scheduleAlarm(long wakeUp) {
409            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
410            if (alarms == null) {
411                Log.e(Constants.TAG, "couldn't get alarm manager");
412                return;
413            }
414
415            if (Constants.LOGV) {
416                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
417            }
418
419            Intent intent = new Intent(Constants.ACTION_RETRY);
420            intent.setClassName("com.android.providers.downloads",
421                    DownloadReceiver.class.getName());
422            alarms.set(
423                    AlarmManager.RTC_WAKEUP,
424                    mSystemFacade.currentTimeMillis() + wakeUp,
425                    PendingIntent.getBroadcast(DownloadService.this, 0, intent,
426                            PendingIntent.FLAG_ONE_SHOT));
427        }
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 insertDownloadLocked(DownloadInfo.Reader reader, long now) {
435        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
436        mDownloads.put(info.mId, info);
437
438        if (Constants.LOGVV) {
439            Log.v(Constants.TAG, "processing inserted download " + info.mId);
440        }
441
442        info.startIfReady(now, mStorageManager);
443        return info;
444    }
445
446    /**
447     * Updates the local copy of the info about a download.
448     */
449    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
450        int oldVisibility = info.mVisibility;
451        int oldStatus = info.mStatus;
452
453        reader.updateFromDatabase(info);
454        if (Constants.LOGVV) {
455            Log.v(Constants.TAG, "processing updated download " + info.mId +
456                    ", status: " + info.mStatus);
457        }
458
459        boolean lostVisibility =
460                oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
461                && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
462                && Downloads.Impl.isStatusCompleted(info.mStatus);
463        boolean justCompleted =
464                !Downloads.Impl.isStatusCompleted(oldStatus)
465                && Downloads.Impl.isStatusCompleted(info.mStatus);
466        if (lostVisibility || justCompleted) {
467            mSystemFacade.cancelNotification(info.mId);
468        }
469
470        info.startIfReady(now, mStorageManager);
471    }
472
473    /**
474     * Removes the local copy of the info about a download.
475     */
476    private void deleteDownloadLocked(long id) {
477        DownloadInfo info = mDownloads.get(id);
478        if (info.shouldScanFile()) {
479            scanFile(info, false, false);
480        }
481        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
482            info.mStatus = Downloads.Impl.STATUS_CANCELED;
483        }
484        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
485            if (Constants.LOGVV) {
486                Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
487            }
488            new File(info.mFileName).delete();
489        }
490        mSystemFacade.cancelNotification(info.mId);
491        mDownloads.remove(info.mId);
492    }
493
494    /**
495     * Attempts to scan the file if necessary.
496     * @return true if the file has been properly scanned.
497     */
498    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
499            final boolean deleteFile) {
500        synchronized (this) {
501            if (mMediaScannerService == null) {
502                // not bound to mediaservice. but if in the process of connecting to it, wait until
503                // connection is resolved
504                while (mMediaScannerConnecting) {
505                    Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
506                    try {
507                        this.wait(WAIT_TIMEOUT);
508                    } catch (InterruptedException e1) {
509                        throw new IllegalStateException("wait interrupted");
510                    }
511                }
512            }
513            // do we have mediaservice?
514            if (mMediaScannerService == null) {
515                // no available MediaService And not even in the process of connecting to it
516                return false;
517            }
518            if (Constants.LOGV) {
519                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
520            }
521            try {
522                final Uri key = info.getAllDownloadsUri();
523                final long id = info.mId;
524                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
525                        new IMediaScannerListener.Stub() {
526                            public void scanCompleted(String path, Uri uri) {
527                                if (updateDatabase) {
528                                    // Mark this as 'scanned' in the database
529                                    // so that it is NOT subject to re-scanning by MediaScanner
530                                    // next time this database row row is encountered
531                                    ContentValues values = new ContentValues();
532                                    values.put(Constants.MEDIA_SCANNED, 1);
533                                    if (uri != null) {
534                                        values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
535                                                uri.toString());
536                                    }
537                                    getContentResolver().update(key, values, null, null);
538                                } else if (deleteFile) {
539                                    if (uri != null) {
540                                        // use the Uri returned to delete it from the MediaProvider
541                                        getContentResolver().delete(uri, null, null);
542                                    }
543                                    // delete the file and delete its row from the downloads db
544                                    deleteFileIfExists(path);
545                                    getContentResolver().delete(
546                                            Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
547                                            Downloads.Impl._ID + " = ? ",
548                                            new String[]{String.valueOf(id)});
549                                }
550                            }
551                        });
552                return true;
553            } catch (RemoteException e) {
554                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
555                return false;
556            }
557        }
558    }
559
560    private void deleteFileIfExists(String path) {
561        try {
562            if (!TextUtils.isEmpty(path)) {
563                if (Constants.LOGVV) {
564                    Log.d(TAG, "deleteFileIfExists() deleting " + path);
565                }
566                File file = new File(path);
567                file.delete();
568            }
569        } catch (Exception e) {
570            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
571        }
572    }
573
574    @Override
575    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
576        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
577        synchronized (mDownloads) {
578            final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
579            Collections.sort(ids);
580            for (Long id : ids) {
581                final DownloadInfo info = mDownloads.get(id);
582                info.dump(pw);
583            }
584        }
585    }
586}
587