DownloadService.java revision d1214c9c92b6a4a44cfc46125c33d071a0f3d880
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 android.text.format.DateUtils.MINUTE_IN_MILLIS;
20import static com.android.providers.downloads.Constants.TAG;
21
22import android.app.AlarmManager;
23import android.app.DownloadManager;
24import android.app.PendingIntent;
25import android.app.Service;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.Intent;
29import android.content.res.Resources;
30import android.database.ContentObserver;
31import android.database.Cursor;
32import android.net.Uri;
33import android.os.Handler;
34import android.os.HandlerThread;
35import android.os.IBinder;
36import android.os.Message;
37import android.os.Process;
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;
47import com.google.common.collect.Sets;
48
49import java.io.File;
50import java.io.FileDescriptor;
51import java.io.PrintWriter;
52import java.util.Collections;
53import java.util.List;
54import java.util.Map;
55import java.util.Set;
56import java.util.concurrent.ExecutorService;
57import java.util.concurrent.LinkedBlockingQueue;
58import java.util.concurrent.ThreadPoolExecutor;
59import java.util.concurrent.TimeUnit;
60
61/**
62 * Performs background downloads as requested by applications that use
63 * {@link DownloadManager}. Multiple start commands can be issued at this
64 * service, and it will continue running until no downloads are being actively
65 * processed. It may schedule alarms to resume downloads in future.
66 * <p>
67 * Any database updates important enough to initiate tasks should always be
68 * delivered through {@link Context#startService(Intent)}.
69 */
70public class DownloadService extends Service {
71    // TODO: migrate WakeLock from individual DownloadThreads out into
72    // DownloadReceiver to protect our entire workflow.
73
74    private static final boolean DEBUG_LIFECYCLE = true;
75
76    @VisibleForTesting
77    SystemFacade mSystemFacade;
78
79    private AlarmManager mAlarmManager;
80    private StorageManager mStorageManager;
81
82    /** Observer to get notified when the content observer's data changes */
83    private DownloadManagerContentObserver mObserver;
84
85    /** Class to handle Notification Manager updates */
86    private DownloadNotifier mNotifier;
87
88    /**
89     * The Service's view of the list of downloads, mapping download IDs to the corresponding info
90     * object. This is kept independently from the content provider, and the Service only initiates
91     * downloads based on this data, so that it can deal with situation where the data in the
92     * content provider changes or disappears.
93     */
94    @GuardedBy("mDownloads")
95    private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
96
97    private final ExecutorService mExecutor = buildDownloadExecutor();
98
99    private static ExecutorService buildDownloadExecutor() {
100        final int maxConcurrent = Resources.getSystem().getInteger(
101                com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
102
103        // Create a bounded thread pool for executing downloads; it creates
104        // threads as needed (up to maximum) and reclaims them when finished.
105        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
106                maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS,
107                new LinkedBlockingQueue<Runnable>());
108        executor.allowCoreThreadTimeOut(true);
109        return executor;
110    }
111
112    private DownloadScanner mScanner;
113
114    private HandlerThread mUpdateThread;
115    private Handler mUpdateHandler;
116
117    private volatile int mLastStartId;
118
119    /**
120     * Receives notifications when the data in the content provider changes
121     */
122    private class DownloadManagerContentObserver extends ContentObserver {
123        public DownloadManagerContentObserver() {
124            super(new Handler());
125        }
126
127        @Override
128        public void onChange(final boolean selfChange) {
129            enqueueUpdate();
130        }
131    }
132
133    /**
134     * Returns an IBinder instance when someone wants to connect to this
135     * service. Binding to this service is not allowed.
136     *
137     * @throws UnsupportedOperationException
138     */
139    @Override
140    public IBinder onBind(Intent i) {
141        throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
142    }
143
144    /**
145     * Initializes the service when it is first created
146     */
147    @Override
148    public void onCreate() {
149        super.onCreate();
150        if (Constants.LOGVV) {
151            Log.v(Constants.TAG, "Service onCreate");
152        }
153
154        if (mSystemFacade == null) {
155            mSystemFacade = new RealSystemFacade(this);
156        }
157
158        mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
159        mStorageManager = new StorageManager(this);
160
161        mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
162        mUpdateThread.start();
163        mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
164
165        mScanner = new DownloadScanner(this);
166
167        mNotifier = new DownloadNotifier(this);
168        mNotifier.cancelAll();
169
170        mObserver = new DownloadManagerContentObserver();
171        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
172                true, mObserver);
173    }
174
175    @Override
176    public int onStartCommand(Intent intent, int flags, int startId) {
177        int returnValue = super.onStartCommand(intent, flags, startId);
178        if (Constants.LOGVV) {
179            Log.v(Constants.TAG, "Service onStart");
180        }
181        mLastStartId = startId;
182        enqueueUpdate();
183        return returnValue;
184    }
185
186    @Override
187    public void onDestroy() {
188        mUpdateThread.quit();
189        mScanner.shutdown();
190        getContentResolver().unregisterContentObserver(mObserver);
191        if (Constants.LOGVV) {
192            Log.v(Constants.TAG, "Service onDestroy");
193        }
194        super.onDestroy();
195    }
196
197    /**
198     * Enqueue an {@link #updateLocked()} pass to occur in future.
199     */
200    private void enqueueUpdate() {
201        mUpdateHandler.removeMessages(MSG_UPDATE);
202        mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
203    }
204
205    /**
206     * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to
207     * catch any finished operations that didn't trigger an update pass.
208     */
209    private void enqueueFinalUpdate() {
210        mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
211        mUpdateHandler.sendMessageDelayed(
212                mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
213                MINUTE_IN_MILLIS);
214    }
215
216    private static final int MSG_UPDATE = 1;
217    private static final int MSG_FINAL_UPDATE = 2;
218
219    private Handler.Callback mUpdateCallback = new Handler.Callback() {
220        @Override
221        public boolean handleMessage(Message msg) {
222            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
223
224            final int startId = msg.arg1;
225            if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
226
227            // Since database is current source of truth, our "active" status
228            // depends on database state. We always get one final update pass
229            // once the real actions have finished and persisted their state.
230
231            // TODO: switch to asking real tasks to derive active state
232            // TODO: handle media scanner timeouts
233
234            final boolean isActive;
235            synchronized (mDownloads) {
236                isActive = updateLocked();
237            }
238
239            if (msg.what == MSG_FINAL_UPDATE) {
240                Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
241                        + "; someone didn't update correctly.");
242            }
243
244            if (isActive) {
245                // Still doing useful work, keep service alive. These active
246                // tasks will trigger another update pass when they're finished.
247
248                // Enqueue delayed update pass to catch finished operations that
249                // didn't trigger an update pass; these are bugs.
250                enqueueFinalUpdate();
251
252            } else {
253                // No active tasks, and any pending update messages can be
254                // ignored, since any updates important enough to initiate tasks
255                // will always be delivered with a new startId.
256
257                if (stopSelfResult(startId)) {
258                    if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
259                    mUpdateHandler.removeMessages(MSG_UPDATE);
260                    mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
261                }
262            }
263
264            return true;
265        }
266    };
267
268    /**
269     * Update {@link #mDownloads} to match {@link DownloadProvider} state.
270     * Depending on current download state it may enqueue {@link DownloadThread}
271     * instances, request {@link DownloadScanner} scans, update user-visible
272     * notifications, and/or schedule future actions with {@link AlarmManager}.
273     * <p>
274     * Should only be called from {@link #mUpdateThread} as after being
275     * requested through {@link #enqueueUpdate()}.
276     *
277     * @return If there are active tasks being processed, as of the database
278     *         snapshot taken in this update.
279     */
280    private boolean updateLocked() {
281        final long now = mSystemFacade.currentTimeMillis();
282
283        boolean isActive = false;
284        long nextActionMillis = Long.MAX_VALUE;
285
286        final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
287
288        final ContentResolver resolver = getContentResolver();
289        final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
290                null, null, null, null);
291        try {
292            final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
293            final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
294            while (cursor.moveToNext()) {
295                final long id = cursor.getLong(idColumn);
296                staleIds.remove(id);
297
298                DownloadInfo info = mDownloads.get(id);
299                if (info != null) {
300                    updateDownload(reader, info, now);
301                } else {
302                    info = insertDownloadLocked(reader, now);
303                }
304
305                if (info.mDeleted) {
306                    // Delete download if requested, but only after cleaning up
307                    if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
308                        resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
309                    }
310
311                    deleteFileIfExists(info.mFileName);
312                    resolver.delete(info.getAllDownloadsUri(), null, null);
313
314                } else {
315                    // Kick off download task if ready
316                    final boolean activeDownload = info.startDownloadIfReady(mExecutor);
317
318                    // Kick off media scan if completed
319                    final boolean activeScan = info.startScanIfReady(mScanner);
320
321                    if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
322                        Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
323                                + ", activeScan=" + activeScan);
324                    }
325
326                    isActive |= activeDownload;
327                    isActive |= activeScan;
328                }
329
330                // Keep track of nearest next action
331                nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
332            }
333        } finally {
334            cursor.close();
335        }
336
337        // Clean up stale downloads that disappeared
338        for (Long id : staleIds) {
339            deleteDownloadLocked(id);
340        }
341
342        // Update notifications visible to user
343        mNotifier.updateWith(mDownloads.values());
344
345        // Set alarm when next action is in future. It's okay if the service
346        // continues to run in meantime, since it will kick off an update pass.
347        if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
348            if (Constants.LOGV) {
349                Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
350            }
351
352            final Intent intent = new Intent(Constants.ACTION_RETRY);
353            intent.setClass(this, DownloadReceiver.class);
354            mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
355                    PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
356        }
357
358        return isActive;
359    }
360
361    /**
362     * Keeps a local copy of the info about a download, and initiates the
363     * download if appropriate.
364     */
365    private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
366        final DownloadInfo info = reader.newDownloadInfo(
367                this, mSystemFacade, mStorageManager, mNotifier);
368        mDownloads.put(info.mId, info);
369
370        if (Constants.LOGVV) {
371            Log.v(Constants.TAG, "processing inserted download " + info.mId);
372        }
373
374        return info;
375    }
376
377    /**
378     * Updates the local copy of the info about a download.
379     */
380    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
381        reader.updateFromDatabase(info);
382        if (Constants.LOGVV) {
383            Log.v(Constants.TAG, "processing updated download " + info.mId +
384                    ", status: " + info.mStatus);
385        }
386    }
387
388    /**
389     * Removes the local copy of the info about a download.
390     */
391    private void deleteDownloadLocked(long id) {
392        DownloadInfo info = mDownloads.get(id);
393        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
394            info.mStatus = Downloads.Impl.STATUS_CANCELED;
395        }
396        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
397            if (Constants.LOGVV) {
398                Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
399            }
400            deleteFileIfExists(info.mFileName);
401        }
402        mDownloads.remove(info.mId);
403    }
404
405    private void deleteFileIfExists(String path) {
406        if (!TextUtils.isEmpty(path)) {
407            if (Constants.LOGVV) {
408                Log.d(TAG, "deleteFileIfExists() deleting " + path);
409            }
410            final File file = new File(path);
411            if (file.exists() && !file.delete()) {
412                Log.w(TAG, "file: '" + path + "' couldn't be deleted");
413            }
414        }
415    }
416
417    @Override
418    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
419        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
420        synchronized (mDownloads) {
421            final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
422            Collections.sort(ids);
423            for (Long id : ids) {
424                final DownloadInfo info = mDownloads.get(id);
425                info.dump(pw);
426            }
427        }
428    }
429}
430