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