DownloadService.java revision 5218d33d57990c3e3549c58bd3f0ac244dfc3d59
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 305 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 306 long id = cursor.getLong(idColumn); 307 idsNoLongerInDatabase.remove(id); 308 DownloadInfo info = mDownloads.get(id); 309 if (info != null) { 310 updateDownload(reader, info, now); 311 } else { 312 info = insertDownload(reader, now); 313 } 314 315 if (info.shouldScanFile() && !scanFile(info, true, false)) { 316 mustScan = true; 317 keepService = true; 318 } 319 if (info.hasCompletionNotification()) { 320 keepService = true; 321 } 322 long next = info.nextAction(now); 323 if (next == 0) { 324 keepService = true; 325 } else if (next > 0 && next < wakeUp) { 326 wakeUp = next; 327 } 328 } 329 } finally { 330 cursor.close(); 331 } 332 333 for (Long id : idsNoLongerInDatabase) { 334 deleteDownload(id); 335 } 336 337 // is there a need to start the DownloadService? yes, if there are rows to be 338 // deleted. 339 if (!mustScan) { 340 for (DownloadInfo info : mDownloads.values()) { 341 if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) { 342 mustScan = true; 343 keepService = true; 344 break; 345 } 346 } 347 } 348 mNotifier.updateNotification(mDownloads.values()); 349 if (mustScan) { 350 bindMediaScanner(); 351 } else { 352 mMediaScannerConnection.disconnectMediaScanner(); 353 } 354 355 // look for all rows with deleted flag set and delete the rows from the database 356 // permanently 357 for (DownloadInfo info : mDownloads.values()) { 358 if (info.mDeleted) { 359 // this row is to be deleted from the database. but does it have 360 // mediaProviderUri? 361 if (TextUtils.isEmpty(info.mMediaProviderUri)) { 362 if (info.shouldScanFile()) { 363 // initiate rescan of the file to - which will populate 364 // mediaProviderUri column in this row 365 if (!scanFile(info, false, true)) { 366 throw new IllegalStateException("scanFile failed!"); 367 } 368 continue; 369 } 370 } else { 371 // yes it has mediaProviderUri column already filled in. 372 // delete it from MediaProvider database. 373 getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null, 374 null); 375 } 376 // delete the file 377 deleteFileIfExists(info.mFileName); 378 // delete from the downloads db 379 getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 380 Downloads.Impl._ID + " = ? ", 381 new String[]{String.valueOf(info.mId)}); 382 } 383 } 384 } 385 } 386 387 private void bindMediaScanner() { 388 if (!mMediaScannerConnecting) { 389 Intent intent = new Intent(); 390 intent.setClassName("com.android.providers.media", 391 "com.android.providers.media.MediaScannerService"); 392 mMediaScannerConnecting = true; 393 bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE); 394 } 395 } 396 397 private void scheduleAlarm(long wakeUp) { 398 AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 399 if (alarms == null) { 400 Log.e(Constants.TAG, "couldn't get alarm manager"); 401 return; 402 } 403 404 if (Constants.LOGV) { 405 Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); 406 } 407 408 Intent intent = new Intent(Constants.ACTION_RETRY); 409 intent.setClassName("com.android.providers.downloads", 410 DownloadReceiver.class.getName()); 411 alarms.set( 412 AlarmManager.RTC_WAKEUP, 413 mSystemFacade.currentTimeMillis() + wakeUp, 414 PendingIntent.getBroadcast(DownloadService.this, 0, intent, 415 PendingIntent.FLAG_ONE_SHOT)); 416 } 417 } 418 419 /** 420 * Keeps a local copy of the info about a download, and initiates the 421 * download if appropriate. 422 */ 423 private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) { 424 DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade); 425 mDownloads.put(info.mId, info); 426 427 if (Constants.LOGVV) { 428 info.logVerboseInfo(); 429 } 430 431 info.startIfReady(now, mStorageManager); 432 return info; 433 } 434 435 /** 436 * Updates the local copy of the info about a download. 437 */ 438 private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { 439 int oldVisibility = info.mVisibility; 440 int oldStatus = info.mStatus; 441 442 reader.updateFromDatabase(info); 443 444 boolean lostVisibility = 445 oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED 446 && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED 447 && Downloads.Impl.isStatusCompleted(info.mStatus); 448 boolean justCompleted = 449 !Downloads.Impl.isStatusCompleted(oldStatus) 450 && Downloads.Impl.isStatusCompleted(info.mStatus); 451 if (lostVisibility || justCompleted) { 452 mSystemFacade.cancelNotification(info.mId); 453 } 454 455 info.startIfReady(now, mStorageManager); 456 } 457 458 /** 459 * Removes the local copy of the info about a download. 460 */ 461 private void deleteDownload(long id) { 462 DownloadInfo info = mDownloads.get(id); 463 if (info.shouldScanFile()) { 464 scanFile(info, false, false); 465 } 466 if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { 467 info.mStatus = Downloads.Impl.STATUS_CANCELED; 468 } 469 if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { 470 new File(info.mFileName).delete(); 471 } 472 mSystemFacade.cancelNotification(info.mId); 473 mDownloads.remove(info.mId); 474 } 475 476 /** 477 * Attempts to scan the file if necessary. 478 * @return true if the file has been properly scanned. 479 */ 480 private boolean scanFile(DownloadInfo info, final boolean updateDatabase, 481 final boolean deleteFile) { 482 synchronized (this) { 483 if (mMediaScannerService == null) { 484 // not bound to mediaservice. but if in the process of connecting to it, wait until 485 // connection is resolved 486 while (mMediaScannerConnecting) { 487 Log.d(Constants.TAG, "waiting for mMediaScannerService service: "); 488 try { 489 this.wait(WAIT_TIMEOUT); 490 } catch (InterruptedException e1) { 491 throw new IllegalStateException("wait interrupted"); 492 } 493 } 494 } 495 // do we have mediaservice? 496 if (mMediaScannerService == null) { 497 // no available MediaService And not even in the process of connecting to it 498 return false; 499 } 500 if (Constants.LOGV) { 501 Log.v(Constants.TAG, "Scanning file " + info.mFileName); 502 } 503 try { 504 final Uri key = info.getAllDownloadsUri(); 505 final String mimeType = info.mMimeType; 506 final ContentResolver resolver = getContentResolver(); 507 final long id = info.mId; 508 mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType, 509 new IMediaScannerListener.Stub() { 510 public void scanCompleted(String path, Uri uri) { 511 if (updateDatabase) { 512 // Mark this as 'scanned' in the database 513 // so that it is NOT subject to re-scanning by MediaScanner 514 // next time this database row row is encountered 515 ContentValues values = new ContentValues(); 516 values.put(Constants.MEDIA_SCANNED, 1); 517 if (uri != null) { 518 values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, 519 uri.toString()); 520 } 521 getContentResolver().update(key, values, null, null); 522 } else if (deleteFile) { 523 if (uri != null) { 524 // use the Uri returned to delete it from the MediaProvider 525 getContentResolver().delete(uri, null, null); 526 } 527 // delete the file and delete its row from the downloads db 528 deleteFileIfExists(path); 529 getContentResolver().delete( 530 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 531 Downloads.Impl._ID + " = ? ", 532 new String[]{String.valueOf(id)}); 533 } 534 } 535 }); 536 return true; 537 } catch (RemoteException e) { 538 Log.w(Constants.TAG, "Failed to scan file " + info.mFileName); 539 return false; 540 } 541 } 542 } 543 544 private void deleteFileIfExists(String path) { 545 try { 546 if (!TextUtils.isEmpty(path)) { 547 File file = new File(path); 548 file.delete(); 549 } 550 } catch (Exception e) { 551 Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); 552 } 553 } 554} 555