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