• 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 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      */
getInstance(Context context)83     synchronized static StorageManager getInstance(Context context) {
84         if (sSingleton == null) {
85             sSingleton = new StorageManager(context);
86         }
87         return sSingleton;
88     }
89 
StorageManager(Context context)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 
incrementNumDownloadsSoFar()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;
startThreadToCleanupDatabaseAndPurgeFileSystem()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 
verifySpaceBeforeWritingToFile(int destination, String path, long length)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 
verifySpace(int destination, String path, long length)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      */
findSpace(File root, long targetBytes, int destination)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      */
getAvailableBytesInDownloadsDataDir(File root)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 
getAvailableBytesInFileSystemAtGivenRoot(File root)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 
locateDestinationDirectory(String mimeType, int destination, long contentLength)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 
getDownloadDataDirectory()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      */
discardPurgeableFiles(int destination, long targetBytes)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      */
removeSpuriousFiles()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      */
trimDatabase()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 
incrementBytesDownloadedSinceLastCheckOnSpace(long val)459     private synchronized int incrementBytesDownloadedSinceLastCheckOnSpace(long val) {
460         mBytesDownloadedSinceLastCheckOnSpace += val;
461         return mBytesDownloadedSinceLastCheckOnSpace;
462     }
463 
resetBytesDownloadedSinceLastCheckOnSpace()464     private synchronized void resetBytesDownloadedSinceLastCheckOnSpace() {
465         mBytesDownloadedSinceLastCheckOnSpace = 0;
466     }
467 }
468