DownloadService.java revision 703bc3a83056a878a83e263b992fb5331b84535f
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                    mUpdateThread.quit();
272                }
273            }
274
275            return true;
276        }
277    };
278
279    /**
280     * Update {@link #mDownloads} to match {@link DownloadProvider} state.
281     * Depending on current download state it may enqueue {@link DownloadThread}
282     * instances, request {@link DownloadScanner} scans, update user-visible
283     * notifications, and/or schedule future actions with {@link AlarmManager}.
284     * <p>
285     * Should only be called from {@link #mUpdateThread} as after being
286     * requested through {@link #enqueueUpdate()}.
287     *
288     * @return If there are active tasks being processed, as of the database
289     *         snapshot taken in this update.
290     */
291    private boolean updateLocked() {
292        final long now = mSystemFacade.currentTimeMillis();
293
294        boolean isActive = false;
295        long nextActionMillis = Long.MAX_VALUE;
296
297        final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
298
299        final ContentResolver resolver = getContentResolver();
300        final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
301                null, null, null, null);
302        try {
303            final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
304            final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
305            while (cursor.moveToNext()) {
306                final long id = cursor.getLong(idColumn);
307                staleIds.remove(id);
308
309                DownloadInfo info = mDownloads.get(id);
310                if (info != null) {
311                    updateDownload(reader, info, now);
312                } else {
313                    info = insertDownloadLocked(reader, now);
314                }
315
316                if (info.mDeleted) {
317                    // Delete download if requested, but only after cleaning up
318                    if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
319                        resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
320                    }
321
322                    deleteFileIfExists(info.mFileName);
323                    resolver.delete(info.getAllDownloadsUri(), null, null);
324
325                } else {
326                    // Kick off download task if ready
327                    final boolean activeDownload = info.startDownloadIfReady(mExecutor);
328
329                    // Kick off media scan if completed
330                    final boolean activeScan = info.startScanIfReady(mScanner);
331
332                    if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
333                        Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
334                                + ", activeScan=" + activeScan);
335                    }
336
337                    isActive |= activeDownload;
338                    isActive |= activeScan;
339                }
340
341                // Keep track of nearest next action
342                nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
343            }
344        } finally {
345            cursor.close();
346        }
347
348        // Clean up stale downloads that disappeared
349        for (Long id : staleIds) {
350            deleteDownloadLocked(id);
351        }
352
353        // Update notifications visible to user
354        mNotifier.updateWith(mDownloads.values());
355
356        // Set alarm when next action is in future. It's okay if the service
357        // continues to run in meantime, since it will kick off an update pass.
358        if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
359            if (Constants.LOGV) {
360                Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
361            }
362
363            final Intent intent = new Intent(Constants.ACTION_RETRY);
364            intent.setClass(this, DownloadReceiver.class);
365            mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
366                    PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
367        }
368
369        return isActive;
370    }
371
372    /**
373     * Keeps a local copy of the info about a download, and initiates the
374     * download if appropriate.
375     */
376    private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
377        final DownloadInfo info = reader.newDownloadInfo(
378                this, mSystemFacade, mStorageManager, mNotifier);
379        mDownloads.put(info.mId, info);
380
381        if (Constants.LOGVV) {
382            Log.v(Constants.TAG, "processing inserted download " + info.mId);
383        }
384
385        return info;
386    }
387
388    /**
389     * Updates the local copy of the info about a download.
390     */
391    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
392        reader.updateFromDatabase(info);
393        if (Constants.LOGVV) {
394            Log.v(Constants.TAG, "processing updated download " + info.mId +
395                    ", status: " + info.mStatus);
396        }
397    }
398
399    /**
400     * Removes the local copy of the info about a download.
401     */
402    private void deleteDownloadLocked(long id) {
403        DownloadInfo info = mDownloads.get(id);
404        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
405            info.mStatus = Downloads.Impl.STATUS_CANCELED;
406        }
407        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
408            if (Constants.LOGVV) {
409                Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
410            }
411            deleteFileIfExists(info.mFileName);
412        }
413        mDownloads.remove(info.mId);
414    }
415
416    private void deleteFileIfExists(String path) {
417        if (!TextUtils.isEmpty(path)) {
418            if (Constants.LOGVV) {
419                Log.d(TAG, "deleteFileIfExists() deleting " + path);
420            }
421            final File file = new File(path);
422            if (file.exists() && !file.delete()) {
423                Log.w(TAG, "file: '" + path + "' couldn't be deleted");
424            }
425        }
426    }
427
428    @Override
429    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
430        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
431        synchronized (mDownloads) {
432            final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
433            Collections.sort(ids);
434            for (Long id : ids) {
435                final DownloadInfo info = mDownloads.get(id);
436                info.dump(pw);
437            }
438        }
439    }
440}
441