DownloadService.java revision a40a349c0107660bfb4004467550725a3ca3ec97
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 DownloadNotifier 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 DownloadNotifier(this); 224 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.updateWith(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 info.startIfReady(now, mStorageManager); 459 } 460 461 /** 462 * Removes the local copy of the info about a download. 463 */ 464 private void deleteDownloadLocked(long id) { 465 DownloadInfo info = mDownloads.get(id); 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 if (Constants.LOGVV) { 471 Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); 472 } 473 new File(info.mFileName).delete(); 474 } 475 mDownloads.remove(info.mId); 476 } 477 478 /** 479 * Attempts to scan the file if necessary. 480 * @return true if the file has been properly scanned. 481 */ 482 private boolean scanFile(DownloadInfo info, final boolean updateDatabase, 483 final boolean deleteFile) { 484 synchronized (this) { 485 if (mMediaScannerService == null) { 486 // not bound to mediaservice. but if in the process of connecting to it, wait until 487 // connection is resolved 488 while (mMediaScannerConnecting) { 489 Log.d(Constants.TAG, "waiting for mMediaScannerService service: "); 490 try { 491 this.wait(WAIT_TIMEOUT); 492 } catch (InterruptedException e1) { 493 throw new IllegalStateException("wait interrupted"); 494 } 495 } 496 } 497 // do we have mediaservice? 498 if (mMediaScannerService == null) { 499 // no available MediaService And not even in the process of connecting to it 500 return false; 501 } 502 if (Constants.LOGV) { 503 Log.v(Constants.TAG, "Scanning file " + info.mFileName); 504 } 505 try { 506 final Uri key = info.getAllDownloadsUri(); 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 if (Constants.LOGVV) { 548 Log.d(TAG, "deleteFileIfExists() deleting " + path); 549 } 550 File file = new File(path); 551 file.delete(); 552 } 553 } catch (Exception e) { 554 Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); 555 } 556 } 557 558 @Override 559 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 560 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); 561 synchronized (mDownloads) { 562 final List<Long> ids = Lists.newArrayList(mDownloads.keySet()); 563 Collections.sort(ids); 564 for (Long id : ids) { 565 final DownloadInfo info = mDownloads.get(id); 566 info.dump(pw); 567 } 568 } 569 } 570} 571