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