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