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