DownloadService.java revision a40a349c0107660bfb4004467550725a3ca3ec97
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 DownloadNotifier 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 DownloadNotifier(this);
224
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.updateWith(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        info.startIfReady(now, mStorageManager);
459    }
460
461    /**
462     * Removes the local copy of the info about a download.
463     */
464    private void deleteDownloadLocked(long id) {
465        DownloadInfo info = mDownloads.get(id);
466        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
467            info.mStatus = Downloads.Impl.STATUS_CANCELED;
468        }
469        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
470            if (Constants.LOGVV) {
471                Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
472            }
473            new File(info.mFileName).delete();
474        }
475        mDownloads.remove(info.mId);
476    }
477
478    /**
479     * Attempts to scan the file if necessary.
480     * @return true if the file has been properly scanned.
481     */
482    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
483            final boolean deleteFile) {
484        synchronized (this) {
485            if (mMediaScannerService == null) {
486                // not bound to mediaservice. but if in the process of connecting to it, wait until
487                // connection is resolved
488                while (mMediaScannerConnecting) {
489                    Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
490                    try {
491                        this.wait(WAIT_TIMEOUT);
492                    } catch (InterruptedException e1) {
493                        throw new IllegalStateException("wait interrupted");
494                    }
495                }
496            }
497            // do we have mediaservice?
498            if (mMediaScannerService == null) {
499                // no available MediaService And not even in the process of connecting to it
500                return false;
501            }
502            if (Constants.LOGV) {
503                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
504            }
505            try {
506                final Uri key = info.getAllDownloadsUri();
507                final long id = info.mId;
508                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
509                        new IMediaScannerListener.Stub() {
510                            public void scanCompleted(String path, Uri uri) {
511                                if (updateDatabase) {
512                                    // Mark this as 'scanned' in the database
513                                    // so that it is NOT subject to re-scanning by MediaScanner
514                                    // next time this database row row is encountered
515                                    ContentValues values = new ContentValues();
516                                    values.put(Constants.MEDIA_SCANNED, 1);
517                                    if (uri != null) {
518                                        values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
519                                                uri.toString());
520                                    }
521                                    getContentResolver().update(key, values, null, null);
522                                } else if (deleteFile) {
523                                    if (uri != null) {
524                                        // use the Uri returned to delete it from the MediaProvider
525                                        getContentResolver().delete(uri, null, null);
526                                    }
527                                    // delete the file and delete its row from the downloads db
528                                    deleteFileIfExists(path);
529                                    getContentResolver().delete(
530                                            Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
531                                            Downloads.Impl._ID + " = ? ",
532                                            new String[]{String.valueOf(id)});
533                                }
534                            }
535                        });
536                return true;
537            } catch (RemoteException e) {
538                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
539                return false;
540            }
541        }
542    }
543
544    private void deleteFileIfExists(String path) {
545        try {
546            if (!TextUtils.isEmpty(path)) {
547                if (Constants.LOGVV) {
548                    Log.d(TAG, "deleteFileIfExists() deleting " + path);
549                }
550                File file = new File(path);
551                file.delete();
552            }
553        } catch (Exception e) {
554            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
555        }
556    }
557
558    @Override
559    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
560        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
561        synchronized (mDownloads) {
562            final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
563            Collections.sort(ids);
564            for (Long id : ids) {
565                final DownloadInfo info = mDownloads.get(id);
566                info.dump(pw);
567            }
568        }
569    }
570}
571