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