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