DownloadService.java revision 38648831a92295e9a11831e19e5a9dab4cbd939e
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 com.android.providers.downloads.Constants.TAG; 20 21import android.app.AlarmManager; 22import android.app.PendingIntent; 23import android.app.Service; 24import android.content.ComponentName; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.Intent; 28import android.content.ServiceConnection; 29import android.content.res.Resources; 30import android.database.ContentObserver; 31import android.database.Cursor; 32import android.media.IMediaScannerListener; 33import android.media.IMediaScannerService; 34import android.net.Uri; 35import android.os.Handler; 36import android.os.IBinder; 37import android.os.Process; 38import android.os.RemoteException; 39import android.provider.Downloads; 40import android.text.TextUtils; 41import android.util.Log; 42 43import com.android.internal.annotations.GuardedBy; 44import com.android.internal.util.IndentingPrintWriter; 45import com.google.android.collect.Maps; 46import com.google.common.annotations.VisibleForTesting; 47import com.google.common.collect.Lists; 48 49import java.io.File; 50import java.io.FileDescriptor; 51import java.io.PrintWriter; 52import java.util.Collections; 53import java.util.HashSet; 54import java.util.List; 55import java.util.Map; 56import java.util.Set; 57import java.util.concurrent.ExecutorService; 58import java.util.concurrent.LinkedBlockingQueue; 59import java.util.concurrent.ThreadPoolExecutor; 60import java.util.concurrent.TimeUnit; 61 62/** 63 * Performs the background downloads requested by applications that use the Downloads provider. 64 */ 65public class DownloadService extends Service { 66 /** amount of time to wait to connect to MediaScannerService before timing out */ 67 private static final long WAIT_TIMEOUT = 10 * 1000; 68 69 /** Observer to get notified when the content observer's data changes */ 70 private DownloadManagerContentObserver mObserver; 71 72 /** Class to handle Notification Manager updates */ 73 private DownloadNotifier mNotifier; 74 75 /** 76 * The Service's view of the list of downloads, mapping download IDs to the corresponding info 77 * object. This is kept independently from the content provider, and the Service only initiates 78 * downloads based on this data, so that it can deal with situation where the data in the 79 * content provider changes or disappears. 80 */ 81 @GuardedBy("mDownloads") 82 private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); 83 84 private final ExecutorService mExecutor = buildDownloadExecutor(); 85 86 private static ExecutorService buildDownloadExecutor() { 87 final int maxConcurrent = Resources.getSystem().getInteger( 88 com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed); 89 90 // Create a bounded thread pool for executing downloads; it creates 91 // threads as needed (up to maximum) and reclaims them when finished. 92 final ThreadPoolExecutor executor = new ThreadPoolExecutor( 93 maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS, 94 new LinkedBlockingQueue<Runnable>()); 95 executor.allowCoreThreadTimeOut(true); 96 return executor; 97 } 98 99 /** 100 * The thread that updates the internal download list from the content 101 * provider. 102 */ 103 private UpdateThread mUpdateThread; 104 105 /** 106 * Whether the internal download list should be updated from the content 107 * provider. 108 */ 109 private boolean mPendingUpdate; 110 111 /** 112 * The ServiceConnection object that tells us when we're connected to and disconnected from 113 * the Media Scanner 114 */ 115 private MediaScannerConnection mMediaScannerConnection; 116 117 private boolean mMediaScannerConnecting; 118 119 /** 120 * The IPC interface to the Media Scanner 121 */ 122 private IMediaScannerService mMediaScannerService; 123 124 @VisibleForTesting 125 SystemFacade mSystemFacade; 126 127 private StorageManager mStorageManager; 128 129 /** 130 * Receives notifications when the data in the content provider changes 131 */ 132 private class DownloadManagerContentObserver extends ContentObserver { 133 134 public DownloadManagerContentObserver() { 135 super(new Handler()); 136 } 137 138 /** 139 * Receives notification when the data in the observed content 140 * provider changes. 141 */ 142 @Override 143 public void onChange(final boolean selfChange) { 144 if (Constants.LOGVV) { 145 Log.v(Constants.TAG, "Service ContentObserver received notification"); 146 } 147 updateFromProvider(); 148 } 149 150 } 151 152 /** 153 * Gets called back when the connection to the media 154 * scanner is established or lost. 155 */ 156 public class MediaScannerConnection implements ServiceConnection { 157 public void onServiceConnected(ComponentName className, IBinder service) { 158 if (Constants.LOGVV) { 159 Log.v(Constants.TAG, "Connected to Media Scanner"); 160 } 161 synchronized (DownloadService.this) { 162 try { 163 mMediaScannerConnecting = false; 164 mMediaScannerService = IMediaScannerService.Stub.asInterface(service); 165 if (mMediaScannerService != null) { 166 updateFromProvider(); 167 } 168 } finally { 169 // notify anyone waiting on successful connection to MediaService 170 DownloadService.this.notifyAll(); 171 } 172 } 173 } 174 175 public void disconnectMediaScanner() { 176 synchronized (DownloadService.this) { 177 mMediaScannerConnecting = false; 178 if (mMediaScannerService != null) { 179 mMediaScannerService = null; 180 if (Constants.LOGVV) { 181 Log.v(Constants.TAG, "Disconnecting from Media Scanner"); 182 } 183 try { 184 unbindService(this); 185 } catch (IllegalArgumentException ex) { 186 Log.w(Constants.TAG, "unbindService failed: " + ex); 187 } finally { 188 // notify anyone waiting on unsuccessful connection to MediaService 189 DownloadService.this.notifyAll(); 190 } 191 } 192 } 193 } 194 195 public void onServiceDisconnected(ComponentName className) { 196 try { 197 if (Constants.LOGVV) { 198 Log.v(Constants.TAG, "Disconnected from Media Scanner"); 199 } 200 } finally { 201 synchronized (DownloadService.this) { 202 mMediaScannerService = null; 203 mMediaScannerConnecting = false; 204 // notify anyone waiting on disconnect from MediaService 205 DownloadService.this.notifyAll(); 206 } 207 } 208 } 209 } 210 211 /** 212 * Returns an IBinder instance when someone wants to connect to this 213 * service. Binding to this service is not allowed. 214 * 215 * @throws UnsupportedOperationException 216 */ 217 @Override 218 public IBinder onBind(Intent i) { 219 throw new UnsupportedOperationException("Cannot bind to Download Manager Service"); 220 } 221 222 /** 223 * Initializes the service when it is first created 224 */ 225 @Override 226 public void onCreate() { 227 super.onCreate(); 228 if (Constants.LOGVV) { 229 Log.v(Constants.TAG, "Service onCreate"); 230 } 231 232 if (mSystemFacade == null) { 233 mSystemFacade = new RealSystemFacade(this); 234 } 235 236 mObserver = new DownloadManagerContentObserver(); 237 getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 238 true, mObserver); 239 240 mMediaScannerService = null; 241 mMediaScannerConnecting = false; 242 mMediaScannerConnection = new MediaScannerConnection(); 243 244 mNotifier = new DownloadNotifier(this); 245 mNotifier.cancelAll(); 246 247 mStorageManager = new StorageManager(this); 248 updateFromProvider(); 249 } 250 251 @Override 252 public int onStartCommand(Intent intent, int flags, int startId) { 253 int returnValue = super.onStartCommand(intent, flags, startId); 254 if (Constants.LOGVV) { 255 Log.v(Constants.TAG, "Service onStart"); 256 } 257 updateFromProvider(); 258 return returnValue; 259 } 260 261 /** 262 * Cleans up when the service is destroyed 263 */ 264 @Override 265 public void onDestroy() { 266 getContentResolver().unregisterContentObserver(mObserver); 267 if (Constants.LOGVV) { 268 Log.v(Constants.TAG, "Service onDestroy"); 269 } 270 super.onDestroy(); 271 } 272 273 /** 274 * Parses data from the content provider into private array 275 */ 276 private void updateFromProvider() { 277 synchronized (this) { 278 mPendingUpdate = true; 279 if (mUpdateThread == null) { 280 mUpdateThread = new UpdateThread(); 281 mUpdateThread.start(); 282 } 283 } 284 } 285 286 private class UpdateThread extends Thread { 287 public UpdateThread() { 288 super("Download Service"); 289 } 290 291 @Override 292 public void run() { 293 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 294 boolean keepService = false; 295 // for each update from the database, remember which download is 296 // supposed to get restarted soonest in the future 297 long wakeUp = Long.MAX_VALUE; 298 for (;;) { 299 synchronized (DownloadService.this) { 300 if (mUpdateThread != this) { 301 throw new IllegalStateException( 302 "multiple UpdateThreads in DownloadService"); 303 } 304 if (!mPendingUpdate) { 305 mUpdateThread = null; 306 if (!keepService) { 307 stopSelf(); 308 } 309 if (wakeUp != Long.MAX_VALUE) { 310 scheduleAlarm(wakeUp); 311 } 312 return; 313 } 314 mPendingUpdate = false; 315 } 316 317 synchronized (mDownloads) { 318 long now = mSystemFacade.currentTimeMillis(); 319 boolean mustScan = false; 320 keepService = false; 321 wakeUp = Long.MAX_VALUE; 322 Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet()); 323 324 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 325 null, null, null, null); 326 if (cursor == null) { 327 continue; 328 } 329 try { 330 DownloadInfo.Reader reader = 331 new DownloadInfo.Reader(getContentResolver(), cursor); 332 int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 333 if (Constants.LOGVV) { 334 Log.i(Constants.TAG, "number of rows from downloads-db: " + 335 cursor.getCount()); 336 } 337 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 338 long id = cursor.getLong(idColumn); 339 idsNoLongerInDatabase.remove(id); 340 DownloadInfo info = mDownloads.get(id); 341 if (info != null) { 342 updateDownload(reader, info, now); 343 } else { 344 info = insertDownloadLocked(reader, now); 345 } 346 347 if (info.shouldScanFile() && !scanFile(info, true, false)) { 348 mustScan = true; 349 keepService = true; 350 } 351 if (info.hasCompletionNotification()) { 352 keepService = true; 353 } 354 long next = info.nextAction(now); 355 if (next == 0) { 356 keepService = true; 357 } else if (next > 0 && next < wakeUp) { 358 wakeUp = next; 359 } 360 } 361 } finally { 362 cursor.close(); 363 } 364 365 for (Long id : idsNoLongerInDatabase) { 366 deleteDownloadLocked(id); 367 } 368 369 // is there a need to start the DownloadService? yes, if there are rows to be 370 // deleted. 371 if (!mustScan) { 372 for (DownloadInfo info : mDownloads.values()) { 373 if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) { 374 mustScan = true; 375 keepService = true; 376 break; 377 } 378 } 379 } 380 mNotifier.updateWith(mDownloads.values()); 381 if (mustScan) { 382 bindMediaScanner(); 383 } else { 384 mMediaScannerConnection.disconnectMediaScanner(); 385 } 386 387 // look for all rows with deleted flag set and delete the rows from the database 388 // permanently 389 for (DownloadInfo info : mDownloads.values()) { 390 if (info.mDeleted) { 391 // this row is to be deleted from the database. but does it have 392 // mediaProviderUri? 393 if (TextUtils.isEmpty(info.mMediaProviderUri)) { 394 if (info.shouldScanFile()) { 395 // initiate rescan of the file to - which will populate 396 // mediaProviderUri column in this row 397 if (!scanFile(info, false, true)) { 398 throw new IllegalStateException("scanFile failed!"); 399 } 400 continue; 401 } 402 } else { 403 // yes it has mediaProviderUri column already filled in. 404 // delete it from MediaProvider database. 405 getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null, 406 null); 407 } 408 // delete the file 409 deleteFileIfExists(info.mFileName); 410 // delete from the downloads db 411 getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 412 Downloads.Impl._ID + " = ? ", 413 new String[]{String.valueOf(info.mId)}); 414 } 415 } 416 } 417 } 418 } 419 420 private void bindMediaScanner() { 421 if (!mMediaScannerConnecting) { 422 Intent intent = new Intent(); 423 intent.setClassName("com.android.providers.media", 424 "com.android.providers.media.MediaScannerService"); 425 mMediaScannerConnecting = true; 426 bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE); 427 } 428 } 429 430 private void scheduleAlarm(long wakeUp) { 431 AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 432 if (alarms == null) { 433 Log.e(Constants.TAG, "couldn't get alarm manager"); 434 return; 435 } 436 437 if (Constants.LOGV) { 438 Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); 439 } 440 441 Intent intent = new Intent(Constants.ACTION_RETRY); 442 intent.setClassName("com.android.providers.downloads", 443 DownloadReceiver.class.getName()); 444 alarms.set( 445 AlarmManager.RTC_WAKEUP, 446 mSystemFacade.currentTimeMillis() + wakeUp, 447 PendingIntent.getBroadcast(DownloadService.this, 0, intent, 448 PendingIntent.FLAG_ONE_SHOT)); 449 } 450 } 451 452 /** 453 * Keeps a local copy of the info about a download, and initiates the 454 * download if appropriate. 455 */ 456 private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) { 457 final DownloadInfo info = reader.newDownloadInfo( 458 this, mSystemFacade, mStorageManager, mNotifier); 459 mDownloads.put(info.mId, info); 460 461 if (Constants.LOGVV) { 462 Log.v(Constants.TAG, "processing inserted download " + info.mId); 463 } 464 465 info.startIfReady(mExecutor); 466 return info; 467 } 468 469 /** 470 * Updates the local copy of the info about a download. 471 */ 472 private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { 473 int oldVisibility = info.mVisibility; 474 int oldStatus = info.mStatus; 475 476 reader.updateFromDatabase(info); 477 if (Constants.LOGVV) { 478 Log.v(Constants.TAG, "processing updated download " + info.mId + 479 ", status: " + info.mStatus); 480 } 481 info.startIfReady(mExecutor); 482 } 483 484 /** 485 * Removes the local copy of the info about a download. 486 */ 487 private void deleteDownloadLocked(long id) { 488 DownloadInfo info = mDownloads.get(id); 489 if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { 490 info.mStatus = Downloads.Impl.STATUS_CANCELED; 491 } 492 if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { 493 if (Constants.LOGVV) { 494 Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); 495 } 496 new File(info.mFileName).delete(); 497 } 498 mDownloads.remove(info.mId); 499 } 500 501 /** 502 * Attempts to scan the file if necessary. 503 * @return true if the file has been properly scanned. 504 */ 505 private boolean scanFile(DownloadInfo info, final boolean updateDatabase, 506 final boolean deleteFile) { 507 synchronized (this) { 508 if (mMediaScannerService == null) { 509 // not bound to mediaservice. but if in the process of connecting to it, wait until 510 // connection is resolved 511 while (mMediaScannerConnecting) { 512 Log.d(Constants.TAG, "waiting for mMediaScannerService service: "); 513 try { 514 this.wait(WAIT_TIMEOUT); 515 } catch (InterruptedException e1) { 516 throw new IllegalStateException("wait interrupted"); 517 } 518 } 519 } 520 // do we have mediaservice? 521 if (mMediaScannerService == null) { 522 // no available MediaService And not even in the process of connecting to it 523 return false; 524 } 525 if (Constants.LOGV) { 526 Log.v(Constants.TAG, "Scanning file " + info.mFileName); 527 } 528 try { 529 final Uri key = info.getAllDownloadsUri(); 530 final long id = info.mId; 531 mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType, 532 new IMediaScannerListener.Stub() { 533 public void scanCompleted(String path, Uri uri) { 534 if (updateDatabase) { 535 // Mark this as 'scanned' in the database 536 // so that it is NOT subject to re-scanning by MediaScanner 537 // next time this database row row is encountered 538 ContentValues values = new ContentValues(); 539 values.put(Constants.MEDIA_SCANNED, 1); 540 if (uri != null) { 541 values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, 542 uri.toString()); 543 } 544 getContentResolver().update(key, values, null, null); 545 } else if (deleteFile) { 546 if (uri != null) { 547 // use the Uri returned to delete it from the MediaProvider 548 getContentResolver().delete(uri, null, null); 549 } 550 // delete the file and delete its row from the downloads db 551 deleteFileIfExists(path); 552 getContentResolver().delete( 553 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 554 Downloads.Impl._ID + " = ? ", 555 new String[]{String.valueOf(id)}); 556 } 557 } 558 }); 559 return true; 560 } catch (RemoteException e) { 561 Log.w(Constants.TAG, "Failed to scan file " + info.mFileName); 562 return false; 563 } 564 } 565 } 566 567 private void deleteFileIfExists(String path) { 568 try { 569 if (!TextUtils.isEmpty(path)) { 570 if (Constants.LOGVV) { 571 Log.d(TAG, "deleteFileIfExists() deleting " + path); 572 } 573 File file = new File(path); 574 file.delete(); 575 } 576 } catch (Exception e) { 577 Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); 578 } 579 } 580 581 @Override 582 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 583 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); 584 synchronized (mDownloads) { 585 final List<Long> ids = Lists.newArrayList(mDownloads.keySet()); 586 Collections.sort(ids); 587 for (Long id : ids) { 588 final DownloadInfo info = mDownloads.get(id); 589 info.dump(pw); 590 } 591 } 592 } 593} 594