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