diff options
Diffstat (limited to 'src/com/android/providers/downloads/DownloadService.java')
-rw-r--r-- | src/com/android/providers/downloads/DownloadService.java | 516 |
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); - } - } - } -} |