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