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