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