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