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