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