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