blob: 58cf380cfb6e056aa458306b31d15c173b7ebab2 [file] [log] [blame]
/*
* 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.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Process;
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;
/** 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);
mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
mUpdateThread.start();
mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
mScanner = new DownloadScanner(this);
mNotifier = new DownloadNotifier(this);
mNotifier.cancelAll();
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();
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();
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);
}
}
}
}