1 /* 2 * Copyright (C) 2010 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.LOGV; 20 import static com.android.providers.downloads.Constants.TAG; 21 22 import android.content.ContentUris; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.database.sqlite.SQLiteException; 27 import android.net.Uri; 28 import android.os.Environment; 29 import android.os.StatFs; 30 import android.provider.Downloads; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.util.Slog; 34 35 import com.android.internal.R; 36 37 import java.io.File; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.List; 41 42 import libcore.io.ErrnoException; 43 import libcore.io.Libcore; 44 import libcore.io.StructStat; 45 46 /** 47 * Manages the storage space consumed by Downloads Data dir. When space falls below 48 * a threshold limit (set in resource xml files), starts cleanup of the Downloads data dir 49 * to free up space. 50 */ 51 class StorageManager { 52 /** the max amount of space allowed to be taken up by the downloads data dir */ 53 private static final long sMaxdownloadDataDirSize = 54 Resources.getSystem().getInteger(R.integer.config_downloadDataDirSize) * 1024 * 1024; 55 56 /** threshold (in bytes) beyond which the low space warning kicks in and attempt is made to 57 * purge some downloaded files to make space 58 */ 59 private static final long sDownloadDataDirLowSpaceThreshold = 60 Resources.getSystem().getInteger( 61 R.integer.config_downloadDataDirLowSpaceThreshold) 62 * sMaxdownloadDataDirSize / 100; 63 64 /** see {@link Environment#getExternalStorageDirectory()} */ 65 private final File mExternalStorageDir; 66 67 /** see {@link Environment#getDownloadCacheDirectory()} */ 68 private final File mSystemCacheDir; 69 70 /** The downloaded files are saved to this dir. it is the value returned by 71 * {@link Context#getCacheDir()}. 72 */ 73 private final File mDownloadDataDir; 74 75 /** the Singleton instance of this class. 76 * TODO: once DownloadService is refactored into a long-living object, there is no need 77 * for this Singleton'ing. 78 */ 79 private static StorageManager sSingleton = null; 80 81 /** how often do we need to perform checks on space to make sure space is available */ 82 private static final int FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY = 1024 * 1024; // 1MB 83 private int mBytesDownloadedSinceLastCheckOnSpace = 0; 84 85 /** misc members */ 86 private final Context mContext; 87 88 /** 89 * maintains Singleton instance of this class 90 */ 91 synchronized static StorageManager getInstance(Context context) { 92 if (sSingleton == null) { 93 sSingleton = new StorageManager(context); 94 } 95 return sSingleton; 96 } 97 98 private StorageManager(Context context) { // constructor is private 99 mContext = context; 100 mDownloadDataDir = context.getCacheDir(); 101 mExternalStorageDir = Environment.getExternalStorageDirectory(); 102 mSystemCacheDir = Environment.getDownloadCacheDirectory(); 103 startThreadToCleanupDatabaseAndPurgeFileSystem(); 104 } 105 106 /** How often should database and filesystem be cleaned up to remove spurious files 107 * from the file system and 108 * The value is specified in terms of num of downloads since last time the cleanup was done. 109 */ 110 private static final int FREQUENCY_OF_DATABASE_N_FILESYSTEM_CLEANUP = 250; 111 private int mNumDownloadsSoFar = 0; 112 113 synchronized void incrementNumDownloadsSoFar() { 114 if (++mNumDownloadsSoFar % FREQUENCY_OF_DATABASE_N_FILESYSTEM_CLEANUP == 0) { 115 startThreadToCleanupDatabaseAndPurgeFileSystem(); 116 } 117 } 118 /* start a thread to cleanup the following 119 * remove spurious files from the file system 120 * remove excess entries from the database 121 */ 122 private Thread mCleanupThread = null; 123 private synchronized void startThreadToCleanupDatabaseAndPurgeFileSystem() { 124 if (mCleanupThread != null && mCleanupThread.isAlive()) { 125 return; 126 } 127 mCleanupThread = new Thread() { 128 @Override public void run() { 129 removeSpuriousFiles(); 130 trimDatabase(); 131 } 132 }; 133 mCleanupThread.start(); 134 } 135 136 void verifySpaceBeforeWritingToFile(int destination, String path, long length) 137 throws StopRequestException { 138 // do this check only once for every 1MB of downloaded data 139 if (incrementBytesDownloadedSinceLastCheckOnSpace(length) < 140 FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY) { 141 return; 142 } 143 verifySpace(destination, path, length); 144 } 145 146 void verifySpace(int destination, String path, long length) throws StopRequestException { 147 resetBytesDownloadedSinceLastCheckOnSpace(); 148 File dir = null; 149 if (Constants.LOGV) { 150 Log.i(Constants.TAG, "in verifySpace, destination: " + destination + 151 ", path: " + path + ", length: " + length); 152 } 153 if (path == null) { 154 throw new IllegalArgumentException("path can't be null"); 155 } 156 switch (destination) { 157 case Downloads.Impl.DESTINATION_CACHE_PARTITION: 158 case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: 159 case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: 160 dir = mDownloadDataDir; 161 break; 162 case Downloads.Impl.DESTINATION_EXTERNAL: 163 dir = mExternalStorageDir; 164 break; 165 case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: 166 dir = mSystemCacheDir; 167 break; 168 case Downloads.Impl.DESTINATION_FILE_URI: 169 if (path.startsWith(mExternalStorageDir.getPath())) { 170 dir = mExternalStorageDir; 171 } else if (path.startsWith(mDownloadDataDir.getPath())) { 172 dir = mDownloadDataDir; 173 } else if (path.startsWith(mSystemCacheDir.getPath())) { 174 dir = mSystemCacheDir; 175 } 176 break; 177 } 178 if (dir == null) { 179 throw new IllegalStateException("invalid combination of destination: " + destination + 180 ", path: " + path); 181 } 182 findSpace(dir, length, destination); 183 } 184 185 /** 186 * finds space in the given filesystem (input param: root) to accommodate # of bytes 187 * specified by the input param(targetBytes). 188 * returns true if found. false otherwise. 189 */ 190 private synchronized void findSpace(File root, long targetBytes, int destination) 191 throws StopRequestException { 192 if (targetBytes == 0) { 193 return; 194 } 195 if (destination == Downloads.Impl.DESTINATION_FILE_URI || 196 destination == Downloads.Impl.DESTINATION_EXTERNAL) { 197 if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 198 throw new StopRequestException(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR, 199 "external media not mounted"); 200 } 201 } 202 // is there enough space in the file system of the given param 'root'. 203 long bytesAvailable = getAvailableBytesInFileSystemAtGivenRoot(root); 204 if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) { 205 /* filesystem's available space is below threshold for low space warning. 206 * threshold typically is 10% of download data dir space quota. 207 * try to cleanup and see if the low space situation goes away. 208 */ 209 discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold); 210 removeSpuriousFiles(); 211 bytesAvailable = getAvailableBytesInFileSystemAtGivenRoot(root); 212 if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) { 213 /* 214 * available space is still below the threshold limit. 215 * 216 * If this is system cache dir, print a warning. 217 * otherwise, don't allow downloading until more space 218 * is available because downloadmanager shouldn't end up taking those last 219 * few MB of space left on the filesystem. 220 */ 221 if (root.equals(mSystemCacheDir)) { 222 Log.w(Constants.TAG, "System cache dir ('/cache') is running low on space." + 223 "space available (in bytes): " + bytesAvailable); 224 } else { 225 throw new StopRequestException(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, 226 "space in the filesystem rooted at: " + root + 227 " is below 10% availability. stopping this download."); 228 } 229 } 230 } 231 if (root.equals(mDownloadDataDir)) { 232 // this download is going into downloads data dir. check space in that specific dir. 233 bytesAvailable = getAvailableBytesInDownloadsDataDir(mDownloadDataDir); 234 if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) { 235 // print a warning 236 Log.w(Constants.TAG, "Downloads data dir: " + root + 237 " is running low on space. space available (in bytes): " + bytesAvailable); 238 } 239 if (bytesAvailable < targetBytes) { 240 // Insufficient space; make space. 241 discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold); 242 removeSpuriousFiles(); 243 bytesAvailable = getAvailableBytesInDownloadsDataDir(mDownloadDataDir); 244 } 245 } 246 if (bytesAvailable < targetBytes) { 247 throw new StopRequestException(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, 248 "not enough free space in the filesystem rooted at: " + root + 249 " and unable to free any more"); 250 } 251 } 252 253 /** 254 * returns the number of bytes available in the downloads data dir 255 * TODO this implementation is too slow. optimize it. 256 */ 257 private long getAvailableBytesInDownloadsDataDir(File root) { 258 File[] files = root.listFiles(); 259 long space = sMaxdownloadDataDirSize; 260 if (files == null) { 261 return space; 262 } 263 int size = files.length; 264 for (int i = 0; i < size; i++) { 265 space -= files[i].length(); 266 } 267 if (Constants.LOGV) { 268 Log.i(Constants.TAG, "available space (in bytes) in downloads data dir: " + space); 269 } 270 return space; 271 } 272 273 private long getAvailableBytesInFileSystemAtGivenRoot(File root) { 274 StatFs stat = new StatFs(root.getPath()); 275 // put a bit of margin (in case creating the file grows the system by a few blocks) 276 long availableBlocks = (long) stat.getAvailableBlocks() - 4; 277 long size = stat.getBlockSize() * availableBlocks; 278 if (Constants.LOGV) { 279 Log.i(Constants.TAG, "available space (in bytes) in filesystem rooted at: " + 280 root.getPath() + " is: " + size); 281 } 282 return size; 283 } 284 285 File locateDestinationDirectory(String mimeType, int destination, long contentLength) 286 throws StopRequestException { 287 switch (destination) { 288 case Downloads.Impl.DESTINATION_CACHE_PARTITION: 289 case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: 290 case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: 291 return mDownloadDataDir; 292 case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: 293 return mSystemCacheDir; 294 case Downloads.Impl.DESTINATION_EXTERNAL: 295 File base = new File(mExternalStorageDir.getPath() + Constants.DEFAULT_DL_SUBDIR); 296 if (!base.isDirectory() && !base.mkdir()) { 297 // Can't create download directory, e.g. because a file called "download" 298 // already exists at the root level, or the SD card filesystem is read-only. 299 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 300 "unable to create external downloads directory " + base.getPath()); 301 } 302 return base; 303 default: 304 throw new IllegalStateException("unexpected value for destination: " + destination); 305 } 306 } 307 308 File getDownloadDataDirectory() { 309 return mDownloadDataDir; 310 } 311 312 /** 313 * Deletes purgeable files from the cache partition. This also deletes 314 * the matching database entries. Files are deleted in LRU order until 315 * the total byte size is greater than targetBytes 316 */ 317 private long discardPurgeableFiles(int destination, long targetBytes) { 318 if (true || Constants.LOGV) { 319 Log.i(Constants.TAG, "discardPurgeableFiles: destination = " + destination + 320 ", targetBytes = " + targetBytes); 321 } 322 String destStr = (destination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) ? 323 String.valueOf(destination) : 324 String.valueOf(Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE); 325 String[] bindArgs = new String[]{destStr}; 326 Cursor cursor = mContext.getContentResolver().query( 327 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 328 null, 329 "( " + 330 Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " + 331 Downloads.Impl.COLUMN_DESTINATION + " = ? )", 332 bindArgs, 333 Downloads.Impl.COLUMN_LAST_MODIFICATION); 334 if (cursor == null) { 335 return 0; 336 } 337 long totalFreed = 0; 338 try { 339 final int dataIndex = cursor.getColumnIndex(Downloads.Impl._DATA); 340 while (cursor.moveToNext() && totalFreed < targetBytes) { 341 final String data = cursor.getString(dataIndex); 342 if (TextUtils.isEmpty(data)) continue; 343 344 File file = new File(data); 345 if (true || Constants.LOGV) { 346 Slog.d(Constants.TAG, "purging " + file.getAbsolutePath() + " for " + 347 file.length() + " bytes"); 348 } 349 totalFreed += file.length(); 350 file.delete(); 351 long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID)); 352 mContext.getContentResolver().delete( 353 ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), 354 null, null); 355 } 356 } finally { 357 cursor.close(); 358 } 359 if (true || Constants.LOGV) { 360 Log.i(Constants.TAG, "Purged files, freed " + totalFreed + " for " + 361 targetBytes + " requested"); 362 } 363 return totalFreed; 364 } 365 366 /** 367 * Removes files in the systemcache and downloads data dir without corresponding entries in 368 * the downloads database. 369 * This can occur if a delete is done on the database but the file is not removed from the 370 * filesystem (due to sudden death of the process, for example). 371 * This is not a very common occurrence. So, do this only once in a while. 372 */ 373 private void removeSpuriousFiles() { 374 if (true || Constants.LOGV) { 375 Log.i(Constants.TAG, "in removeSpuriousFiles"); 376 } 377 // get a list of all files in system cache dir and downloads data dir 378 List<File> files = new ArrayList<File>(); 379 File[] listOfFiles = mSystemCacheDir.listFiles(); 380 if (listOfFiles != null) { 381 files.addAll(Arrays.asList(listOfFiles)); 382 } 383 listOfFiles = mDownloadDataDir.listFiles(); 384 if (listOfFiles != null) { 385 files.addAll(Arrays.asList(listOfFiles)); 386 } 387 if (files.size() == 0) { 388 return; 389 } 390 Cursor cursor = mContext.getContentResolver().query( 391 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 392 new String[] { Downloads.Impl._DATA }, null, null, null); 393 try { 394 if (cursor != null) { 395 while (cursor.moveToNext()) { 396 String filename = cursor.getString(0); 397 if (!TextUtils.isEmpty(filename)) { 398 if (LOGV) { 399 Log.i(Constants.TAG, "in removeSpuriousFiles, preserving file " + 400 filename); 401 } 402 files.remove(new File(filename)); 403 } 404 } 405 } 406 } finally { 407 if (cursor != null) { 408 cursor.close(); 409 } 410 } 411 412 // delete files owned by us, but that don't appear in our database 413 final int myUid = android.os.Process.myUid(); 414 for (File file : files) { 415 final String path = file.getAbsolutePath(); 416 try { 417 final StructStat stat = Libcore.os.stat(path); 418 if (stat.st_uid == myUid) { 419 Slog.d(TAG, "deleting spurious file " + path); 420 file.delete(); 421 } 422 } catch (ErrnoException e) { 423 Log.w(TAG, "stat(" + path + ") result: " + e); 424 } 425 } 426 } 427 428 /** 429 * Drops old rows from the database to prevent it from growing too large 430 * TODO logic in this method needs to be optimized. maintain the number of downloads 431 * in memory - so that this method can limit the amount of data read. 432 */ 433 private void trimDatabase() { 434 if (Constants.LOGV) { 435 Log.i(Constants.TAG, "in trimDatabase"); 436 } 437 Cursor cursor = null; 438 try { 439 cursor = mContext.getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 440 new String[] { Downloads.Impl._ID }, 441 Downloads.Impl.COLUMN_STATUS + " >= '200'", null, 442 Downloads.Impl.COLUMN_LAST_MODIFICATION); 443 if (cursor == null) { 444 // This isn't good - if we can't do basic queries in our database, 445 // nothing's gonna work 446 Log.e(Constants.TAG, "null cursor in trimDatabase"); 447 return; 448 } 449 if (cursor.moveToFirst()) { 450 int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS; 451 int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 452 while (numDelete > 0) { 453 Uri downloadUri = ContentUris.withAppendedId( 454 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId)); 455 mContext.getContentResolver().delete(downloadUri, null, null); 456 if (!cursor.moveToNext()) { 457 break; 458 } 459 numDelete--; 460 } 461 } 462 } catch (SQLiteException e) { 463 // trimming the database raised an exception. alright, ignore the exception 464 // and return silently. trimming database is not exactly a critical operation 465 // and there is no need to propagate the exception. 466 Log.w(Constants.TAG, "trimDatabase failed with exception: " + e.getMessage()); 467 return; 468 } finally { 469 if (cursor != null) { 470 cursor.close(); 471 } 472 } 473 } 474 475 private synchronized int incrementBytesDownloadedSinceLastCheckOnSpace(long val) { 476 mBytesDownloadedSinceLastCheckOnSpace += val; 477 return mBytesDownloadedSinceLastCheckOnSpace; 478 } 479 480 private synchronized void resetBytesDownloadedSinceLastCheckOnSpace() { 481 mBytesDownloadedSinceLastCheckOnSpace = 0; 482 } 483 } 484