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