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