DownloadService.java revision dffbb9c4567e9d29d19964a83129e38dceab7055
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.content.ContentResolver; 27import android.content.Context; 28import android.content.Intent; 29import android.content.res.Resources; 30import android.database.ContentObserver; 31import android.database.Cursor; 32import android.net.Uri; 33import android.os.Handler; 34import android.os.HandlerThread; 35import android.os.IBinder; 36import android.os.Message; 37import android.os.Process; 38import android.provider.Downloads; 39import android.text.TextUtils; 40import android.util.Log; 41 42import com.android.internal.annotations.GuardedBy; 43import com.android.internal.util.IndentingPrintWriter; 44import com.google.android.collect.Maps; 45import com.google.common.annotations.VisibleForTesting; 46import com.google.common.collect.Lists; 47import com.google.common.collect.Sets; 48 49import java.io.File; 50import java.io.FileDescriptor; 51import java.io.PrintWriter; 52import java.util.Arrays; 53import java.util.Collections; 54import java.util.List; 55import java.util.Map; 56import java.util.Set; 57import java.util.concurrent.CancellationException; 58import java.util.concurrent.ExecutionException; 59import java.util.concurrent.ExecutorService; 60import java.util.concurrent.Future; 61import java.util.concurrent.LinkedBlockingQueue; 62import java.util.concurrent.ThreadPoolExecutor; 63import java.util.concurrent.TimeUnit; 64 65/** 66 * Performs background downloads as requested by applications that use 67 * {@link DownloadManager}. Multiple start commands can be issued at this 68 * service, and it will continue running until no downloads are being actively 69 * processed. It may schedule alarms to resume downloads in future. 70 * <p> 71 * Any database updates important enough to initiate tasks should always be 72 * delivered through {@link Context#startService(Intent)}. 73 */ 74public class DownloadService extends Service { 75 // TODO: migrate WakeLock from individual DownloadThreads out into 76 // DownloadReceiver to protect our entire workflow. 77 78 private static final boolean DEBUG_LIFECYCLE = false; 79 80 @VisibleForTesting 81 SystemFacade mSystemFacade; 82 83 private AlarmManager mAlarmManager; 84 85 /** Observer to get notified when the content observer's data changes */ 86 private DownloadManagerContentObserver mObserver; 87 88 /** Class to handle Notification Manager updates */ 89 private DownloadNotifier mNotifier; 90 91 /** 92 * The Service's view of the list of downloads, mapping download IDs to the corresponding info 93 * object. This is kept independently from the content provider, and the Service only initiates 94 * downloads based on this data, so that it can deal with situation where the data in the 95 * content provider changes or disappears. 96 */ 97 @GuardedBy("mDownloads") 98 private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); 99 100 private final ExecutorService mExecutor = buildDownloadExecutor(); 101 102 private static ExecutorService buildDownloadExecutor() { 103 final int maxConcurrent = Resources.getSystem().getInteger( 104 com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed); 105 106 // Create a bounded thread pool for executing downloads; it creates 107 // threads as needed (up to maximum) and reclaims them when finished. 108 final ThreadPoolExecutor executor = new ThreadPoolExecutor( 109 maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS, 110 new LinkedBlockingQueue<Runnable>()) { 111 @Override 112 protected void afterExecute(Runnable r, Throwable t) { 113 super.afterExecute(r, t); 114 115 if (t == null && r instanceof Future<?>) { 116 try { 117 ((Future<?>) r).get(); 118 } catch (CancellationException ce) { 119 t = ce; 120 } catch (ExecutionException ee) { 121 t = ee.getCause(); 122 } catch (InterruptedException ie) { 123 Thread.currentThread().interrupt(); 124 } 125 } 126 127 if (t != null) { 128 Log.w(TAG, "Uncaught exception", t); 129 } 130 } 131 }; 132 executor.allowCoreThreadTimeOut(true); 133 return executor; 134 } 135 136 private DownloadScanner mScanner; 137 138 private HandlerThread mUpdateThread; 139 private Handler mUpdateHandler; 140 141 private volatile int mLastStartId; 142 143 /** 144 * Receives notifications when the data in the content provider changes 145 */ 146 private class DownloadManagerContentObserver extends ContentObserver { 147 public DownloadManagerContentObserver() { 148 super(new Handler()); 149 } 150 151 @Override 152 public void onChange(final boolean selfChange) { 153 enqueueUpdate(); 154 } 155 } 156 157 /** 158 * Returns an IBinder instance when someone wants to connect to this 159 * service. Binding to this service is not allowed. 160 * 161 * @throws UnsupportedOperationException 162 */ 163 @Override 164 public IBinder onBind(Intent i) { 165 throw new UnsupportedOperationException("Cannot bind to Download Manager Service"); 166 } 167 168 /** 169 * Initializes the service when it is first created 170 */ 171 @Override 172 public void onCreate() { 173 super.onCreate(); 174 if (Constants.LOGVV) { 175 Log.v(Constants.TAG, "Service onCreate"); 176 } 177 178 if (mSystemFacade == null) { 179 mSystemFacade = new RealSystemFacade(this); 180 } 181 182 mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 183 184 mUpdateThread = new HandlerThread(TAG + "-UpdateThread"); 185 mUpdateThread.start(); 186 mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback); 187 188 mScanner = new DownloadScanner(this); 189 190 mNotifier = new DownloadNotifier(this); 191 mNotifier.cancelAll(); 192 193 mObserver = new DownloadManagerContentObserver(); 194 getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 195 true, mObserver); 196 } 197 198 @Override 199 public int onStartCommand(Intent intent, int flags, int startId) { 200 int returnValue = super.onStartCommand(intent, flags, startId); 201 if (Constants.LOGVV) { 202 Log.v(Constants.TAG, "Service onStart"); 203 } 204 mLastStartId = startId; 205 enqueueUpdate(); 206 return returnValue; 207 } 208 209 @Override 210 public void onDestroy() { 211 getContentResolver().unregisterContentObserver(mObserver); 212 mScanner.shutdown(); 213 mUpdateThread.quit(); 214 if (Constants.LOGVV) { 215 Log.v(Constants.TAG, "Service onDestroy"); 216 } 217 super.onDestroy(); 218 } 219 220 /** 221 * Enqueue an {@link #updateLocked()} pass to occur in future. 222 */ 223 public void enqueueUpdate() { 224 if (mUpdateHandler != null) { 225 mUpdateHandler.removeMessages(MSG_UPDATE); 226 mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget(); 227 } 228 } 229 230 /** 231 * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to 232 * catch any finished operations that didn't trigger an update pass. 233 */ 234 private void enqueueFinalUpdate() { 235 mUpdateHandler.removeMessages(MSG_FINAL_UPDATE); 236 mUpdateHandler.sendMessageDelayed( 237 mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1), 238 5 * MINUTE_IN_MILLIS); 239 } 240 241 private static final int MSG_UPDATE = 1; 242 private static final int MSG_FINAL_UPDATE = 2; 243 244 private Handler.Callback mUpdateCallback = new Handler.Callback() { 245 @Override 246 public boolean handleMessage(Message msg) { 247 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 248 249 final int startId = msg.arg1; 250 if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId); 251 252 // Since database is current source of truth, our "active" status 253 // depends on database state. We always get one final update pass 254 // once the real actions have finished and persisted their state. 255 256 // TODO: switch to asking real tasks to derive active state 257 // TODO: handle media scanner timeouts 258 259 final boolean isActive; 260 synchronized (mDownloads) { 261 isActive = updateLocked(); 262 } 263 264 if (msg.what == MSG_FINAL_UPDATE) { 265 // Dump thread stacks belonging to pool 266 for (Map.Entry<Thread, StackTraceElement[]> entry : 267 Thread.getAllStackTraces().entrySet()) { 268 if (entry.getKey().getName().startsWith("pool")) { 269 Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue())); 270 } 271 } 272 273 // Dump speed and update details 274 mNotifier.dumpSpeeds(); 275 276 Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive 277 + "; someone didn't update correctly."); 278 } 279 280 if (isActive) { 281 // Still doing useful work, keep service alive. These active 282 // tasks will trigger another update pass when they're finished. 283 284 // Enqueue delayed update pass to catch finished operations that 285 // didn't trigger an update pass; these are bugs. 286 enqueueFinalUpdate(); 287 288 } else { 289 // No active tasks, and any pending update messages can be 290 // ignored, since any updates important enough to initiate tasks 291 // will always be delivered with a new startId. 292 293 if (stopSelfResult(startId)) { 294 if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped"); 295 getContentResolver().unregisterContentObserver(mObserver); 296 mScanner.shutdown(); 297 mUpdateThread.quit(); 298 } 299 } 300 301 return true; 302 } 303 }; 304 305 /** 306 * Update {@link #mDownloads} to match {@link DownloadProvider} state. 307 * Depending on current download state it may enqueue {@link DownloadThread} 308 * instances, request {@link DownloadScanner} scans, update user-visible 309 * notifications, and/or schedule future actions with {@link AlarmManager}. 310 * <p> 311 * Should only be called from {@link #mUpdateThread} as after being 312 * requested through {@link #enqueueUpdate()}. 313 * 314 * @return If there are active tasks being processed, as of the database 315 * snapshot taken in this update. 316 */ 317 private boolean updateLocked() { 318 final long now = mSystemFacade.currentTimeMillis(); 319 320 boolean isActive = false; 321 long nextActionMillis = Long.MAX_VALUE; 322 323 final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet()); 324 325 final ContentResolver resolver = getContentResolver(); 326 final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 327 null, null, null, null); 328 try { 329 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 330 final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 331 while (cursor.moveToNext()) { 332 final long id = cursor.getLong(idColumn); 333 staleIds.remove(id); 334 335 DownloadInfo info = mDownloads.get(id); 336 if (info != null) { 337 updateDownload(reader, info, now); 338 } else { 339 info = insertDownloadLocked(reader, now); 340 } 341 342 if (info.mDeleted) { 343 // Delete download if requested, but only after cleaning up 344 if (!TextUtils.isEmpty(info.mMediaProviderUri)) { 345 resolver.delete(Uri.parse(info.mMediaProviderUri), null, null); 346 } 347 348 deleteFileIfExists(info.mFileName); 349 resolver.delete(info.getAllDownloadsUri(), null, null); 350 351 } else { 352 // Kick off download task if ready 353 final boolean activeDownload = info.startDownloadIfReady(mExecutor); 354 355 // Kick off media scan if completed 356 final boolean activeScan = info.startScanIfReady(mScanner); 357 358 if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) { 359 Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload 360 + ", activeScan=" + activeScan); 361 } 362 363 isActive |= activeDownload; 364 isActive |= activeScan; 365 } 366 367 // Keep track of nearest next action 368 nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis); 369 } 370 } finally { 371 cursor.close(); 372 } 373 374 // Clean up stale downloads that disappeared 375 for (Long id : staleIds) { 376 deleteDownloadLocked(id); 377 } 378 379 // Update notifications visible to user 380 mNotifier.updateWith(mDownloads.values()); 381 382 // Set alarm when next action is in future. It's okay if the service 383 // continues to run in meantime, since it will kick off an update pass. 384 if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) { 385 if (Constants.LOGV) { 386 Log.v(TAG, "scheduling start in " + nextActionMillis + "ms"); 387 } 388 389 final Intent intent = new Intent(Constants.ACTION_RETRY); 390 intent.setClass(this, DownloadReceiver.class); 391 mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis, 392 PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)); 393 } 394 395 return isActive; 396 } 397 398 /** 399 * Keeps a local copy of the info about a download, and initiates the 400 * download if appropriate. 401 */ 402 private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) { 403 final DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade, mNotifier); 404 mDownloads.put(info.mId, info); 405 406 if (Constants.LOGVV) { 407 Log.v(Constants.TAG, "processing inserted download " + info.mId); 408 } 409 410 return info; 411 } 412 413 /** 414 * Updates the local copy of the info about a download. 415 */ 416 private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { 417 reader.updateFromDatabase(info); 418 if (Constants.LOGVV) { 419 Log.v(Constants.TAG, "processing updated download " + info.mId + 420 ", status: " + info.mStatus); 421 } 422 } 423 424 /** 425 * Removes the local copy of the info about a download. 426 */ 427 private void deleteDownloadLocked(long id) { 428 DownloadInfo info = mDownloads.get(id); 429 if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { 430 info.mStatus = Downloads.Impl.STATUS_CANCELED; 431 } 432 if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { 433 if (Constants.LOGVV) { 434 Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); 435 } 436 deleteFileIfExists(info.mFileName); 437 } 438 mDownloads.remove(info.mId); 439 } 440 441 private void deleteFileIfExists(String path) { 442 if (!TextUtils.isEmpty(path)) { 443 if (Constants.LOGVV) { 444 Log.d(TAG, "deleteFileIfExists() deleting " + path); 445 } 446 final File file = new File(path); 447 if (file.exists() && !file.delete()) { 448 Log.w(TAG, "file: '" + path + "' couldn't be deleted"); 449 } 450 } 451 } 452 453 @Override 454 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 455 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); 456 synchronized (mDownloads) { 457 final List<Long> ids = Lists.newArrayList(mDownloads.keySet()); 458 Collections.sort(ids); 459 for (Long id : ids) { 460 final DownloadInfo info = mDownloads.get(id); 461 info.dump(pw); 462 } 463 } 464 } 465} 466