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