• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
getInstance(Context context)91     synchronized static StorageManager getInstance(Context context) {
92         if (sSingleton == null) {
93             sSingleton = new StorageManager(context);
94         }
95         return sSingleton;
96     }
97 
StorageManager(Context context)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 
incrementNumDownloadsSoFar()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;
startThreadToCleanupDatabaseAndPurgeFileSystem()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 
verifySpaceBeforeWritingToFile(int destination, String path, long length)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 
verifySpace(int destination, String path, long length)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      */
findSpace(File root, long targetBytes, int destination)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      */
getAvailableBytesInDownloadsDataDir(File root)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 
getAvailableBytesInFileSystemAtGivenRoot(File root)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 
locateDestinationDirectory(String mimeType, int destination, long contentLength)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 
getDownloadDataDirectory()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      */
discardPurgeableFiles(int destination, long targetBytes)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      */
removeSpuriousFiles()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      */
trimDatabase()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 
incrementBytesDownloadedSinceLastCheckOnSpace(long val)475     private synchronized int incrementBytesDownloadedSinceLastCheckOnSpace(long val) {
476         mBytesDownloadedSinceLastCheckOnSpace += val;
477         return mBytesDownloadedSinceLastCheckOnSpace;
478     }
479 
resetBytesDownloadedSinceLastCheckOnSpace()480     private synchronized void resetBytesDownloadedSinceLastCheckOnSpace() {
481         mBytesDownloadedSinceLastCheckOnSpace = 0;
482     }
483 }
484