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 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