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