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