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 
     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 
     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 
     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;
    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 
    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 
    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      */
    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      */
    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 
    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 
    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 
    291     File getDownloadDataDirectory() {
    292         return mDownloadDataDir;
    293     }
    294 
    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      */
    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      */
    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      */
    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 
    464     private synchronized int incrementBytesDownloadedSinceLastCheckOnSpace(long val) {
    465         mBytesDownloadedSinceLastCheckOnSpace += val;
    466         return mBytesDownloadedSinceLastCheckOnSpace;
    467     }
    468 
    469     private synchronized void resetBytesDownloadedSinceLastCheckOnSpace() {
    470         mBytesDownloadedSinceLastCheckOnSpace = 0;
    471     }
    472 }
    473