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