summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/DownloadService.java
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2016-04-20 23:23:09 -0600
committerJeff Sharkey <jsharkey@android.com>2016-04-25 12:59:46 -0600
commit3a5f5eafb34eaa4963c801882148e8f61514a61b (patch)
treec38ae2f58cb39e4e17be37e8eec2fe040b4b6436 /src/com/android/providers/downloads/DownloadService.java
parentdbcd4cfe7f0fed77a77afb1c1d242a508fc5462a (diff)
downloadandroid_packages_providers_DownloadProvider-3a5f5eafb34eaa4963c801882148e8f61514a61b.tar.gz
android_packages_providers_DownloadProvider-3a5f5eafb34eaa4963c801882148e8f61514a61b.tar.bz2
android_packages_providers_DownloadProvider-3a5f5eafb34eaa4963c801882148e8f61514a61b.zip
Move DownloadManager to use JobScheduler.
JobScheduler is in a much better position to coordinate tasks across the platform to optimize battery and RAM usage. This change removes a bunch of manual scheduling logic by representing each download as a separate job with relevant scheduling constraints. Requested network types, retry backoff timing, and newly added charging and idle constraints are plumbed through as job parameters. When a job times out, we halt the download and schedule it to resume later. The majority of downloads should have ETag values to enable resuming like this. Remove local wakelocks, since the platform now acquires and blames our jobs on the requesting app. When an active download is pushing updates to the database, check for both paused and cancelled state to quickly halt an ongoing download. Shift DownloadNotifier to update directly based on a Cursor, since we no longer have the overhead of fully-parsed DownloadInfo objects. Unify a handful of worker threads into a single shared thread. Remove legacy "large download" activity that was thrown in the face of the user; the UX best-practice is to go through notification, and update that dialog to let the user override and continue if under the hard limit. Bug: 28098882, 26571724 Change-Id: I33ebe59b3c2ea9c89ec526f70b1950c734abc4a7
Diffstat (limited to 'src/com/android/providers/downloads/DownloadService.java')
-rw-r--r--src/com/android/providers/downloads/DownloadService.java516
1 files changed, 0 insertions, 516 deletions
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
deleted file mode 100644
index 7d4392e8..00000000
--- a/src/com/android/providers/downloads/DownloadService.java
+++ /dev/null
@@ -1,516 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.downloads;
-
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static com.android.providers.downloads.Constants.TAG;
-
-import android.app.AlarmManager;
-import android.app.DownloadManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.IDeviceIdleController;
-import android.os.Message;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.provider.Downloads;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.IndentingPrintWriter;
-import com.google.android.collect.Maps;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Performs background downloads as requested by applications that use
- * {@link DownloadManager}. Multiple start commands can be issued at this
- * service, and it will continue running until no downloads are being actively
- * processed. It may schedule alarms to resume downloads in future.
- * <p>
- * Any database updates important enough to initiate tasks should always be
- * delivered through {@link Context#startService(Intent)}.
- */
-public class DownloadService extends Service {
- // TODO: migrate WakeLock from individual DownloadThreads out into
- // DownloadReceiver to protect our entire workflow.
-
- private static final boolean DEBUG_LIFECYCLE = false;
-
- @VisibleForTesting
- SystemFacade mSystemFacade;
-
- private AlarmManager mAlarmManager;
- private IDeviceIdleController mDeviceIdleController;
-
- /** Observer to get notified when the content observer's data changes */
- private DownloadManagerContentObserver mObserver;
-
- /** Class to handle Notification Manager updates */
- private DownloadNotifier mNotifier;
-
- /** Scheduling of the periodic cleanup job */
- private JobInfo mCleanupJob;
-
- private static final int CLEANUP_JOB_ID = 1;
- private static final long CLEANUP_JOB_PERIOD = 1000 * 60 * 60 * 24; // one day
- private static ComponentName sCleanupServiceName = new ComponentName(
- DownloadIdleService.class.getPackage().getName(),
- DownloadIdleService.class.getName());
-
- /**
- * The Service's view of the list of downloads, mapping download IDs to the corresponding info
- * object. This is kept independently from the content provider, and the Service only initiates
- * downloads based on this data, so that it can deal with situation where the data in the
- * content provider changes or disappears.
- */
- @GuardedBy("mDownloads")
- private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
-
- private final ExecutorService mExecutor = buildDownloadExecutor();
-
- private static ExecutorService buildDownloadExecutor() {
- final int maxConcurrent = Resources.getSystem().getInteger(
- com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
-
- // Create a bounded thread pool for executing downloads; it creates
- // threads as needed (up to maximum) and reclaims them when finished.
- final ThreadPoolExecutor executor = new ThreadPoolExecutor(
- maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS,
- new LinkedBlockingQueue<Runnable>()) {
- @Override
- protected void afterExecute(Runnable r, Throwable t) {
- super.afterExecute(r, t);
-
- if (t == null && r instanceof Future<?>) {
- try {
- ((Future<?>) r).get();
- } catch (CancellationException ce) {
- t = ce;
- } catch (ExecutionException ee) {
- t = ee.getCause();
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- }
- }
-
- if (t != null) {
- Log.w(TAG, "Uncaught exception", t);
- }
- }
- };
- executor.allowCoreThreadTimeOut(true);
- return executor;
- }
-
- private DownloadScanner mScanner;
-
- private HandlerThread mUpdateThread;
- private Handler mUpdateHandler;
-
- private volatile int mLastStartId;
-
- /**
- * Receives notifications when the data in the content provider changes
- */
- private class DownloadManagerContentObserver extends ContentObserver {
- public DownloadManagerContentObserver() {
- super(new Handler());
- }
-
- @Override
- public void onChange(final boolean selfChange) {
- enqueueUpdate();
- }
- }
-
- /**
- * Returns an IBinder instance when someone wants to connect to this
- * service. Binding to this service is not allowed.
- *
- * @throws UnsupportedOperationException
- */
- @Override
- public IBinder onBind(Intent i) {
- throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
- }
-
- /**
- * Initializes the service when it is first created
- */
- @Override
- public void onCreate() {
- super.onCreate();
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service onCreate");
- }
-
- if (mSystemFacade == null) {
- mSystemFacade = new RealSystemFacade(this);
- }
-
- mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
- try {
- mDeviceIdleController.downloadServiceActive(new Binder());
- } catch (RemoteException e) {
- }
-
- mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
- mUpdateThread.start();
- mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
-
- mScanner = new DownloadScanner(this);
-
- mNotifier = new DownloadNotifier(this);
- mNotifier.init();
-
- mObserver = new DownloadManagerContentObserver();
- getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- true, mObserver);
-
- JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
- if (needToScheduleCleanup(js)) {
- final JobInfo job = new JobInfo.Builder(CLEANUP_JOB_ID, sCleanupServiceName)
- .setPeriodic(CLEANUP_JOB_PERIOD)
- .setRequiresCharging(true)
- .setRequiresDeviceIdle(true)
- .build();
- js.schedule(job);
- }
- }
-
- private boolean needToScheduleCleanup(JobScheduler js) {
- List<JobInfo> myJobs = js.getAllPendingJobs();
- if (myJobs != null) {
- final int N = myJobs.size();
- for (int i = 0; i < N; i++) {
- if (myJobs.get(i).getId() == CLEANUP_JOB_ID) {
- // It's already been (persistently) scheduled; no need to do it again
- return false;
- }
- }
- }
- return true;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- int returnValue = super.onStartCommand(intent, flags, startId);
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service onStart");
- }
- mLastStartId = startId;
- enqueueUpdate();
- return returnValue;
- }
-
- @Override
- public void onDestroy() {
- getContentResolver().unregisterContentObserver(mObserver);
- mScanner.shutdown();
- mUpdateThread.quit();
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service onDestroy");
- }
- super.onDestroy();
- }
-
- /**
- * Enqueue an {@link #updateLocked()} pass to occur in future.
- */
- public void enqueueUpdate() {
- if (mUpdateHandler != null) {
- mUpdateHandler.removeMessages(MSG_UPDATE);
- mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
- }
- }
-
- /**
- * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to
- * catch any finished operations that didn't trigger an update pass.
- */
- private void enqueueFinalUpdate() {
- mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
- mUpdateHandler.sendMessageDelayed(
- mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
- 5 * MINUTE_IN_MILLIS);
- }
-
- private static final int MSG_UPDATE = 1;
- private static final int MSG_FINAL_UPDATE = 2;
-
- private Handler.Callback mUpdateCallback = new Handler.Callback() {
- @Override
- public boolean handleMessage(Message msg) {
- Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
- final int startId = msg.arg1;
- if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
-
- // Since database is current source of truth, our "active" status
- // depends on database state. We always get one final update pass
- // once the real actions have finished and persisted their state.
-
- // TODO: switch to asking real tasks to derive active state
- // TODO: handle media scanner timeouts
-
- final boolean isActive;
- synchronized (mDownloads) {
- isActive = updateLocked();
- }
-
- if (msg.what == MSG_FINAL_UPDATE) {
- // Dump thread stacks belonging to pool
- for (Map.Entry<Thread, StackTraceElement[]> entry :
- Thread.getAllStackTraces().entrySet()) {
- if (entry.getKey().getName().startsWith("pool")) {
- Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue()));
- }
- }
-
- // Dump speed and update details
- mNotifier.dumpSpeeds();
-
- Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
- + "; someone didn't update correctly.");
- }
-
- if (isActive) {
- // Still doing useful work, keep service alive. These active
- // tasks will trigger another update pass when they're finished.
-
- // Enqueue delayed update pass to catch finished operations that
- // didn't trigger an update pass; these are bugs.
- enqueueFinalUpdate();
-
- } else {
- // No active tasks, and any pending update messages can be
- // ignored, since any updates important enough to initiate tasks
- // will always be delivered with a new startId.
-
- if (stopSelfResult(startId)) {
- if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
- getContentResolver().unregisterContentObserver(mObserver);
- mScanner.shutdown();
- try {
- mDeviceIdleController.downloadServiceInactive();
- } catch (RemoteException e) {
- }
- mUpdateThread.quit();
- }
- }
-
- return true;
- }
- };
-
- /**
- * Update {@link #mDownloads} to match {@link DownloadProvider} state.
- * Depending on current download state it may enqueue {@link DownloadThread}
- * instances, request {@link DownloadScanner} scans, update user-visible
- * notifications, and/or schedule future actions with {@link AlarmManager}.
- * <p>
- * Should only be called from {@link #mUpdateThread} as after being
- * requested through {@link #enqueueUpdate()}.
- *
- * @return If there are active tasks being processed, as of the database
- * snapshot taken in this update.
- */
- private boolean updateLocked() {
- final long now = mSystemFacade.currentTimeMillis();
-
- boolean isActive = false;
- long nextActionMillis = Long.MAX_VALUE;
-
- final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
-
- final ContentResolver resolver = getContentResolver();
- final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- null, null, null, null);
- try {
- final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
- final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
- while (cursor.moveToNext()) {
- final long id = cursor.getLong(idColumn);
- staleIds.remove(id);
-
- DownloadInfo info = mDownloads.get(id);
- if (info != null) {
- updateDownload(reader, info, now);
- } else {
- info = insertDownloadLocked(reader, now);
- }
-
- if (info.mDeleted) {
- // Delete download if requested, but only after cleaning up
- if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
- resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
- }
-
- deleteFileIfExists(info.mFileName);
- resolver.delete(info.getAllDownloadsUri(), null, null);
-
- } else {
- // Kick off download task if ready
- final boolean activeDownload = info.startDownloadIfReady(mExecutor);
-
- // Kick off media scan if completed
- final boolean activeScan = info.startScanIfReady(mScanner);
-
- if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
- Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
- + ", activeScan=" + activeScan);
- }
-
- isActive |= activeDownload;
- isActive |= activeScan;
- }
-
- // Keep track of nearest next action
- nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
- }
- } finally {
- cursor.close();
- }
-
- // Clean up stale downloads that disappeared
- for (Long id : staleIds) {
- deleteDownloadLocked(id);
- }
-
- // Update notifications visible to user
- mNotifier.updateWith(mDownloads.values());
-
- // Set alarm when next action is in future. It's okay if the service
- // continues to run in meantime, since it will kick off an update pass.
- if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
- if (Constants.LOGV) {
- Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
- }
-
- final Intent intent = new Intent(Constants.ACTION_RETRY);
- intent.setClass(this, DownloadReceiver.class);
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
- PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
- }
-
- return isActive;
- }
-
- /**
- * Keeps a local copy of the info about a download, and initiates the
- * download if appropriate.
- */
- private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
- final DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade, mNotifier);
- mDownloads.put(info.mId, info);
-
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "processing inserted download " + info.mId);
- }
-
- return info;
- }
-
- /**
- * Updates the local copy of the info about a download.
- */
- private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
- reader.updateFromDatabase(info);
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "processing updated download " + info.mId +
- ", status: " + info.mStatus);
- }
- }
-
- /**
- * Removes the local copy of the info about a download.
- */
- private void deleteDownloadLocked(long id) {
- DownloadInfo info = mDownloads.get(id);
- if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
- info.mStatus = Downloads.Impl.STATUS_CANCELED;
- }
- if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
- if (Constants.LOGVV) {
- Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
- }
- deleteFileIfExists(info.mFileName);
- }
- mDownloads.remove(info.mId);
- }
-
- private void deleteFileIfExists(String path) {
- if (!TextUtils.isEmpty(path)) {
- if (Constants.LOGVV) {
- Log.d(TAG, "deleteFileIfExists() deleting " + path);
- }
- final File file = new File(path);
- if (file.exists() && !file.delete()) {
- Log.w(TAG, "file: '" + path + "' couldn't be deleted");
- }
- }
- }
-
- @Override
- protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
- final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- synchronized (mDownloads) {
- final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
- Collections.sort(ids);
- for (Long id : ids) {
- final DownloadInfo info = mDownloads.get(id);
- info.dump(pw);
- }
- }
- }
-}