DownloadService.java revision ad017bfbbb549cbbaa522038fa46450f0cedf413
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 android.app.AlarmManager; 20import android.app.PendingIntent; 21import android.app.Service; 22import android.content.ComponentName; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.ServiceConnection; 28import android.database.ContentObserver; 29import android.database.Cursor; 30import android.media.IMediaScannerService; 31import android.net.Uri; 32import android.os.Environment; 33import android.os.Handler; 34import android.os.IBinder; 35import android.os.Process; 36import android.os.RemoteException; 37import android.provider.Downloads; 38import android.util.Log; 39 40import com.google.android.collect.Maps; 41import com.google.common.annotations.VisibleForTesting; 42 43import java.io.File; 44import java.util.HashSet; 45import java.util.Iterator; 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 /** Observer to get notified when the content observer's data changes */ 55 private DownloadManagerContentObserver mObserver; 56 57 /** Class to handle Notification Manager updates */ 58 private DownloadNotification mNotifier; 59 60 /** 61 * The Service's view of the list of downloads, mapping download IDs to the corresponding info 62 * object. This is kept independently from the content provider, and the Service only initiates 63 * downloads based on this data, so that it can deal with situation where the data in the 64 * content provider changes or disappears. 65 */ 66 private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); 67 68 /** 69 * The thread that updates the internal download list from the content 70 * provider. 71 */ 72 @VisibleForTesting 73 UpdateThread mUpdateThread; 74 75 /** 76 * Whether the internal download list should be updated from the content 77 * provider. 78 */ 79 private boolean mPendingUpdate; 80 81 /** 82 * The ServiceConnection object that tells us when we're connected to and disconnected from 83 * the Media Scanner 84 */ 85 private MediaScannerConnection mMediaScannerConnection; 86 87 private boolean mMediaScannerConnecting; 88 89 /** 90 * The IPC interface to the Media Scanner 91 */ 92 private IMediaScannerService mMediaScannerService; 93 94 @VisibleForTesting 95 SystemFacade mSystemFacade; 96 97 /** 98 * Receives notifications when the data in the content provider changes 99 */ 100 private class DownloadManagerContentObserver extends ContentObserver { 101 102 public DownloadManagerContentObserver() { 103 super(new Handler()); 104 } 105 106 /** 107 * Receives notification when the data in the observed content 108 * provider changes. 109 */ 110 public void onChange(final boolean selfChange) { 111 if (Constants.LOGVV) { 112 Log.v(Constants.TAG, "Service ContentObserver received notification"); 113 } 114 updateFromProvider(); 115 } 116 117 } 118 119 /** 120 * Gets called back when the connection to the media 121 * scanner is established or lost. 122 */ 123 public class MediaScannerConnection implements ServiceConnection { 124 public void onServiceConnected(ComponentName className, IBinder service) { 125 if (Constants.LOGVV) { 126 Log.v(Constants.TAG, "Connected to Media Scanner"); 127 } 128 mMediaScannerConnecting = false; 129 synchronized (DownloadService.this) { 130 mMediaScannerService = IMediaScannerService.Stub.asInterface(service); 131 if (mMediaScannerService != null) { 132 updateFromProvider(); 133 } 134 } 135 } 136 137 public void disconnectMediaScanner() { 138 synchronized (DownloadService.this) { 139 if (mMediaScannerService != null) { 140 mMediaScannerService = null; 141 if (Constants.LOGVV) { 142 Log.v(Constants.TAG, "Disconnecting from Media Scanner"); 143 } 144 try { 145 unbindService(this); 146 } catch (IllegalArgumentException ex) { 147 if (Constants.LOGV) { 148 Log.v(Constants.TAG, "unbindService threw up: " + ex); 149 } 150 } 151 } 152 } 153 } 154 155 public void onServiceDisconnected(ComponentName className) { 156 if (Constants.LOGVV) { 157 Log.v(Constants.TAG, "Disconnected from Media Scanner"); 158 } 159 synchronized (DownloadService.this) { 160 mMediaScannerService = null; 161 } 162 } 163 } 164 165 /** 166 * Returns an IBinder instance when someone wants to connect to this 167 * service. Binding to this service is not allowed. 168 * 169 * @throws UnsupportedOperationException 170 */ 171 public IBinder onBind(Intent i) { 172 throw new UnsupportedOperationException("Cannot bind to Download Manager Service"); 173 } 174 175 /** 176 * Initializes the service when it is first created 177 */ 178 public void onCreate() { 179 super.onCreate(); 180 if (Constants.LOGVV) { 181 Log.v(Constants.TAG, "Service onCreate"); 182 } 183 184 if (mSystemFacade == null) { 185 mSystemFacade = new RealSystemFacade(this); 186 } 187 188 mObserver = new DownloadManagerContentObserver(); 189 getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 190 true, mObserver); 191 192 mMediaScannerService = null; 193 mMediaScannerConnecting = false; 194 mMediaScannerConnection = new MediaScannerConnection(); 195 196 mNotifier = new DownloadNotification(this, mSystemFacade); 197 mSystemFacade.cancelAllNotifications(); 198 199 updateFromProvider(); 200 } 201 202 @Override 203 public int onStartCommand(Intent intent, int flags, int startId) { 204 int returnValue = super.onStartCommand(intent, flags, startId); 205 if (Constants.LOGVV) { 206 Log.v(Constants.TAG, "Service onStart"); 207 } 208 updateFromProvider(); 209 return returnValue; 210 } 211 212 /** 213 * Cleans up when the service is destroyed 214 */ 215 public void onDestroy() { 216 getContentResolver().unregisterContentObserver(mObserver); 217 if (Constants.LOGVV) { 218 Log.v(Constants.TAG, "Service onDestroy"); 219 } 220 super.onDestroy(); 221 } 222 223 /** 224 * Parses data from the content provider into private array 225 */ 226 private void updateFromProvider() { 227 synchronized (this) { 228 mPendingUpdate = true; 229 if (mUpdateThread == null) { 230 mUpdateThread = new UpdateThread(); 231 mSystemFacade.startThread(mUpdateThread); 232 } 233 } 234 } 235 236 private class UpdateThread extends Thread { 237 public UpdateThread() { 238 super("Download Service"); 239 } 240 241 public void run() { 242 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 243 244 trimDatabase(); 245 removeSpuriousFiles(); 246 247 boolean keepService = false; 248 // for each update from the database, remember which download is 249 // supposed to get restarted soonest in the future 250 long wakeUp = Long.MAX_VALUE; 251 for (;;) { 252 synchronized (DownloadService.this) { 253 if (mUpdateThread != this) { 254 throw new IllegalStateException( 255 "multiple UpdateThreads in DownloadService"); 256 } 257 if (!mPendingUpdate) { 258 mUpdateThread = null; 259 if (!keepService) { 260 stopSelf(); 261 } 262 if (wakeUp != Long.MAX_VALUE) { 263 scheduleAlarm(wakeUp); 264 } 265 return; 266 } 267 mPendingUpdate = false; 268 } 269 270 long now = mSystemFacade.currentTimeMillis(); 271 boolean mustScan = false; 272 keepService = false; 273 wakeUp = Long.MAX_VALUE; 274 Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet()); 275 276 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 277 null, null, null, null); 278 if (cursor == null) { 279 continue; 280 } 281 try { 282 DownloadInfo.Reader reader = 283 new DownloadInfo.Reader(getContentResolver(), cursor); 284 int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 285 286 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 287 long id = cursor.getLong(idColumn); 288 idsNoLongerInDatabase.remove(id); 289 DownloadInfo info = mDownloads.get(id); 290 if (info != null) { 291 updateDownload(reader, info, now); 292 } else { 293 info = insertDownload(reader, now); 294 } 295 296 if (info.shouldScanFile() && !scanFile(info, true)) { 297 mustScan = true; 298 keepService = true; 299 } 300 if (info.hasCompletionNotification()) { 301 keepService = true; 302 } 303 long next = info.nextAction(now); 304 if (next == 0) { 305 keepService = true; 306 } else if (next > 0 && next < wakeUp) { 307 wakeUp = next; 308 } 309 } 310 } finally { 311 cursor.close(); 312 } 313 314 for (Long id : idsNoLongerInDatabase) { 315 deleteDownload(id); 316 } 317 318 mNotifier.updateNotification(mDownloads.values()); 319 320 if (mustScan) { 321 bindMediaScanner(); 322 } else { 323 mMediaScannerConnection.disconnectMediaScanner(); 324 } 325 } 326 } 327 328 private void bindMediaScanner() { 329 if (!mMediaScannerConnecting) { 330 Intent intent = new Intent(); 331 intent.setClassName("com.android.providers.media", 332 "com.android.providers.media.MediaScannerService"); 333 mMediaScannerConnecting = true; 334 bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE); 335 } 336 } 337 338 private void scheduleAlarm(long wakeUp) { 339 AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 340 if (alarms == null) { 341 Log.e(Constants.TAG, "couldn't get alarm manager"); 342 return; 343 } 344 345 if (Constants.LOGV) { 346 Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); 347 } 348 349 Intent intent = new Intent(Constants.ACTION_RETRY); 350 intent.setClassName("com.android.providers.downloads", 351 DownloadReceiver.class.getName()); 352 alarms.set( 353 AlarmManager.RTC_WAKEUP, 354 mSystemFacade.currentTimeMillis() + wakeUp, 355 PendingIntent.getBroadcast(DownloadService.this, 0, intent, 356 PendingIntent.FLAG_ONE_SHOT)); 357 } 358 } 359 360 /** 361 * Removes files that may have been left behind in the cache directory 362 */ 363 private void removeSpuriousFiles() { 364 File[] files = Environment.getDownloadCacheDirectory().listFiles(); 365 if (files == null) { 366 // The cache folder doesn't appear to exist (this is likely the case 367 // when running the simulator). 368 return; 369 } 370 HashSet<String> fileSet = new HashSet<String>(); 371 for (int i = 0; i < files.length; i++) { 372 if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) { 373 continue; 374 } 375 if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) { 376 continue; 377 } 378 fileSet.add(files[i].getPath()); 379 } 380 381 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 382 new String[] { Downloads.Impl._DATA }, null, null, null); 383 if (cursor != null) { 384 if (cursor.moveToFirst()) { 385 do { 386 fileSet.remove(cursor.getString(0)); 387 } while (cursor.moveToNext()); 388 } 389 cursor.close(); 390 } 391 Iterator<String> iterator = fileSet.iterator(); 392 while (iterator.hasNext()) { 393 String filename = iterator.next(); 394 if (Constants.LOGV) { 395 Log.v(Constants.TAG, "deleting spurious file " + filename); 396 } 397 new File(filename).delete(); 398 } 399 } 400 401 /** 402 * Drops old rows from the database to prevent it from growing too large 403 */ 404 private void trimDatabase() { 405 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 406 new String[] { Downloads.Impl._ID }, 407 Downloads.Impl.COLUMN_STATUS + " >= '200'", null, 408 Downloads.Impl.COLUMN_LAST_MODIFICATION); 409 if (cursor == null) { 410 // This isn't good - if we can't do basic queries in our database, nothing's gonna work 411 Log.e(Constants.TAG, "null cursor in trimDatabase"); 412 return; 413 } 414 if (cursor.moveToFirst()) { 415 int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS; 416 int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 417 while (numDelete > 0) { 418 Uri downloadUri = ContentUris.withAppendedId( 419 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId)); 420 getContentResolver().delete(downloadUri, null, null); 421 if (!cursor.moveToNext()) { 422 break; 423 } 424 numDelete--; 425 } 426 } 427 cursor.close(); 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 insertDownload(DownloadInfo.Reader reader, long now) { 435 DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade); 436 mDownloads.put(info.mId, info); 437 438 if (Constants.LOGVV) { 439 info.logVerboseInfo(); 440 } 441 442 if (info.isReadyToStart(now)) { 443 info.start(now); 444 } 445 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 458 boolean lostVisibility = 459 oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED 460 && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED 461 && Downloads.Impl.isStatusCompleted(info.mStatus); 462 boolean justCompleted = 463 !Downloads.Impl.isStatusCompleted(oldStatus) 464 && Downloads.Impl.isStatusCompleted(info.mStatus); 465 if (lostVisibility || justCompleted) { 466 mSystemFacade.cancelNotification(info.mId); 467 } 468 469 if (info.isReadyToRestart(now)) { 470 info.start(now); 471 } 472 } 473 474 /** 475 * Removes the local copy of the info about a download. 476 */ 477 private void deleteDownload(long id) { 478 DownloadInfo info = mDownloads.get(id); 479 if (info.shouldScanFile()) { 480 scanFile(info, 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 new File(info.mFileName).delete(); 487 } 488 mSystemFacade.cancelNotification(info.mId); 489 mDownloads.remove(info.mId); 490 } 491 492 /** 493 * Attempts to scan the file if necessary. 494 * @return true if the file has been properly scanned. 495 */ 496 private boolean scanFile(DownloadInfo info, boolean updateDatabase) { 497 synchronized (this) { 498 if (mMediaScannerService == null) { 499 return false; 500 } 501 try { 502 if (Constants.LOGV) { 503 Log.v(Constants.TAG, "Scanning file " + info.mFileName); 504 } 505 mMediaScannerService.scanFile(info.mFileName, info.mMimeType); 506 if (updateDatabase) { 507 ContentValues values = new ContentValues(); 508 values.put(Constants.MEDIA_SCANNED, 1); 509 getContentResolver().update(info.getAllDownloadsUri(), values, null, null); 510 } 511 return true; 512 } catch (RemoteException e) { 513 Log.d(Constants.TAG, "Failed to scan file " + info.mFileName); 514 return false; 515 } 516 } 517 } 518 519} 520