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 17 package com.android.providers.downloads; 18 19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 20 import static com.android.providers.downloads.Constants.TAG; 21 22 import android.app.AlarmManager; 23 import android.app.DownloadManager; 24 import android.app.PendingIntent; 25 import android.app.Service; 26 import android.app.job.JobInfo; 27 import android.app.job.JobScheduler; 28 import android.content.ComponentName; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.res.Resources; 33 import android.database.ContentObserver; 34 import android.database.Cursor; 35 import android.net.Uri; 36 import android.os.Handler; 37 import android.os.HandlerThread; 38 import android.os.IBinder; 39 import android.os.Message; 40 import android.os.Process; 41 import android.provider.Downloads; 42 import android.text.TextUtils; 43 import android.util.Log; 44 45 import com.android.internal.annotations.GuardedBy; 46 import com.android.internal.util.IndentingPrintWriter; 47 import com.google.android.collect.Maps; 48 import com.google.common.annotations.VisibleForTesting; 49 import com.google.common.collect.Lists; 50 import com.google.common.collect.Sets; 51 52 import java.io.File; 53 import java.io.FileDescriptor; 54 import java.io.PrintWriter; 55 import java.util.Arrays; 56 import java.util.Collections; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.Set; 60 import java.util.concurrent.CancellationException; 61 import java.util.concurrent.ExecutionException; 62 import java.util.concurrent.ExecutorService; 63 import java.util.concurrent.Future; 64 import java.util.concurrent.LinkedBlockingQueue; 65 import java.util.concurrent.ThreadPoolExecutor; 66 import 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 */ 77 public 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 final int N = myJobs.size(); 223 for (int i = 0; i < N; i++) { 224 if (myJobs.get(i).getId() == CLEANUP_JOB_ID) { 225 // It's already been (persistently) scheduled; no need to do it again 226 return false; 227 } 228 } 229 return true; 230 } 231 232 @Override 233 public int onStartCommand(Intent intent, int flags, int startId) { 234 int returnValue = super.onStartCommand(intent, flags, startId); 235 if (Constants.LOGVV) { 236 Log.v(Constants.TAG, "Service onStart"); 237 } 238 mLastStartId = startId; 239 enqueueUpdate(); 240 return returnValue; 241 } 242 243 @Override 244 public void onDestroy() { 245 getContentResolver().unregisterContentObserver(mObserver); 246 mScanner.shutdown(); 247 mUpdateThread.quit(); 248 if (Constants.LOGVV) { 249 Log.v(Constants.TAG, "Service onDestroy"); 250 } 251 super.onDestroy(); 252 } 253 254 /** 255 * Enqueue an {@link #updateLocked()} pass to occur in future. 256 */ 257 public void enqueueUpdate() { 258 if (mUpdateHandler != null) { 259 mUpdateHandler.removeMessages(MSG_UPDATE); 260 mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget(); 261 } 262 } 263 264 /** 265 * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to 266 * catch any finished operations that didn't trigger an update pass. 267 */ 268 private void enqueueFinalUpdate() { 269 mUpdateHandler.removeMessages(MSG_FINAL_UPDATE); 270 mUpdateHandler.sendMessageDelayed( 271 mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1), 272 5 * MINUTE_IN_MILLIS); 273 } 274 275 private static final int MSG_UPDATE = 1; 276 private static final int MSG_FINAL_UPDATE = 2; 277 278 private Handler.Callback mUpdateCallback = new Handler.Callback() { 279 @Override 280 public boolean handleMessage(Message msg) { 281 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 282 283 final int startId = msg.arg1; 284 if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId); 285 286 // Since database is current source of truth, our "active" status 287 // depends on database state. We always get one final update pass 288 // once the real actions have finished and persisted their state. 289 290 // TODO: switch to asking real tasks to derive active state 291 // TODO: handle media scanner timeouts 292 293 final boolean isActive; 294 synchronized (mDownloads) { 295 isActive = updateLocked(); 296 } 297 298 if (msg.what == MSG_FINAL_UPDATE) { 299 // Dump thread stacks belonging to pool 300 for (Map.Entry<Thread, StackTraceElement[]> entry : 301 Thread.getAllStackTraces().entrySet()) { 302 if (entry.getKey().getName().startsWith("pool")) { 303 Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue())); 304 } 305 } 306 307 // Dump speed and update details 308 mNotifier.dumpSpeeds(); 309 310 Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive 311 + "; someone didn't update correctly."); 312 } 313 314 if (isActive) { 315 // Still doing useful work, keep service alive. These active 316 // tasks will trigger another update pass when they're finished. 317 318 // Enqueue delayed update pass to catch finished operations that 319 // didn't trigger an update pass; these are bugs. 320 enqueueFinalUpdate(); 321 322 } else { 323 // No active tasks, and any pending update messages can be 324 // ignored, since any updates important enough to initiate tasks 325 // will always be delivered with a new startId. 326 327 if (stopSelfResult(startId)) { 328 if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped"); 329 getContentResolver().unregisterContentObserver(mObserver); 330 mScanner.shutdown(); 331 mUpdateThread.quit(); 332 } 333 } 334 335 return true; 336 } 337 }; 338 339 /** 340 * Update {@link #mDownloads} to match {@link DownloadProvider} state. 341 * Depending on current download state it may enqueue {@link DownloadThread} 342 * instances, request {@link DownloadScanner} scans, update user-visible 343 * notifications, and/or schedule future actions with {@link AlarmManager}. 344 * <p> 345 * Should only be called from {@link #mUpdateThread} as after being 346 * requested through {@link #enqueueUpdate()}. 347 * 348 * @return If there are active tasks being processed, as of the database 349 * snapshot taken in this update. 350 */ 351 private boolean updateLocked() { 352 final long now = mSystemFacade.currentTimeMillis(); 353 354 boolean isActive = false; 355 long nextActionMillis = Long.MAX_VALUE; 356 357 final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet()); 358 359 final ContentResolver resolver = getContentResolver(); 360 final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 361 null, null, null, null); 362 try { 363 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 364 final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 365 while (cursor.moveToNext()) { 366 final long id = cursor.getLong(idColumn); 367 staleIds.remove(id); 368 369 DownloadInfo info = mDownloads.get(id); 370 if (info != null) { 371 updateDownload(reader, info, now); 372 } else { 373 info = insertDownloadLocked(reader, now); 374 } 375 376 if (info.mDeleted) { 377 // Delete download if requested, but only after cleaning up 378 if (!TextUtils.isEmpty(info.mMediaProviderUri)) { 379 resolver.delete(Uri.parse(info.mMediaProviderUri), null, null); 380 } 381 382 deleteFileIfExists(info.mFileName); 383 resolver.delete(info.getAllDownloadsUri(), null, null); 384 385 } else { 386 // Kick off download task if ready 387 final boolean activeDownload = info.startDownloadIfReady(mExecutor); 388 389 // Kick off media scan if completed 390 final boolean activeScan = info.startScanIfReady(mScanner); 391 392 if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) { 393 Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload 394 + ", activeScan=" + activeScan); 395 } 396 397 isActive |= activeDownload; 398 isActive |= activeScan; 399 } 400 401 // Keep track of nearest next action 402 nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis); 403 } 404 } finally { 405 cursor.close(); 406 } 407 408 // Clean up stale downloads that disappeared 409 for (Long id : staleIds) { 410 deleteDownloadLocked(id); 411 } 412 413 // Update notifications visible to user 414 mNotifier.updateWith(mDownloads.values()); 415 416 // Set alarm when next action is in future. It's okay if the service 417 // continues to run in meantime, since it will kick off an update pass. 418 if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) { 419 if (Constants.LOGV) { 420 Log.v(TAG, "scheduling start in " + nextActionMillis + "ms"); 421 } 422 423 final Intent intent = new Intent(Constants.ACTION_RETRY); 424 intent.setClass(this, DownloadReceiver.class); 425 mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis, 426 PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)); 427 } 428 429 return isActive; 430 } 431 432 /** 433 * Keeps a local copy of the info about a download, and initiates the 434 * download if appropriate. 435 */ 436 private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) { 437 final DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade, mNotifier); 438 mDownloads.put(info.mId, info); 439 440 if (Constants.LOGVV) { 441 Log.v(Constants.TAG, "processing inserted download " + info.mId); 442 } 443 444 return info; 445 } 446 447 /** 448 * Updates the local copy of the info about a download. 449 */ 450 private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { 451 reader.updateFromDatabase(info); 452 if (Constants.LOGVV) { 453 Log.v(Constants.TAG, "processing updated download " + info.mId + 454 ", status: " + info.mStatus); 455 } 456 } 457 458 /** 459 * Removes the local copy of the info about a download. 460 */ 461 private void deleteDownloadLocked(long id) { 462 DownloadInfo info = mDownloads.get(id); 463 if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { 464 info.mStatus = Downloads.Impl.STATUS_CANCELED; 465 } 466 if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { 467 if (Constants.LOGVV) { 468 Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); 469 } 470 deleteFileIfExists(info.mFileName); 471 } 472 mDownloads.remove(info.mId); 473 } 474 475 private void deleteFileIfExists(String path) { 476 if (!TextUtils.isEmpty(path)) { 477 if (Constants.LOGVV) { 478 Log.d(TAG, "deleteFileIfExists() deleting " + path); 479 } 480 final File file = new File(path); 481 if (file.exists() && !file.delete()) { 482 Log.w(TAG, "file: '" + path + "' couldn't be deleted"); 483 } 484 } 485 } 486 487 @Override 488 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 489 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); 490 synchronized (mDownloads) { 491 final List<Long> ids = Lists.newArrayList(mDownloads.keySet()); 492 Collections.sort(ids); 493 for (Long id : ids) { 494 final DownloadInfo info = mDownloads.get(id); 495 info.dump(pw); 496 } 497 } 498 } 499 } 500