am bb611587: (-s ours) Import translations. DO NOT MERGE
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / StorageManager.java
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 android.system.ErrnoException;
42 import android.system.Os;
43 import android.system.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 = 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 }