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