summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/StorageManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/StorageManager.java')
-rw-r--r--src/com/android/providers/downloads/StorageManager.java472
1 files changed, 0 insertions, 472 deletions
diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java
deleted file mode 100644
index b8cd6c79..00000000
--- a/src/com/android/providers/downloads/StorageManager.java
+++ /dev/null
@@ -1,472 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.downloads;
-
-import static com.android.providers.downloads.Constants.LOGV;
-import static com.android.providers.downloads.Constants.TAG;
-
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.net.Uri;
-import android.os.Environment;
-import android.os.StatFs;
-import android.provider.Downloads;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.R;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.StructStat;
-
-/**
- * Manages the storage space consumed by Downloads Data dir. When space falls below
- * a threshold limit (set in resource xml files), starts cleanup of the Downloads data dir
- * to free up space.
- */
-class StorageManager {
- /** the max amount of space allowed to be taken up by the downloads data dir */
- private static final long sMaxdownloadDataDirSize =
- Resources.getSystem().getInteger(R.integer.config_downloadDataDirSize) * 1024 * 1024;
-
- /** threshold (in bytes) beyond which the low space warning kicks in and attempt is made to
- * purge some downloaded files to make space
- */
- private static final long sDownloadDataDirLowSpaceThreshold =
- Resources.getSystem().getInteger(
- R.integer.config_downloadDataDirLowSpaceThreshold)
- * sMaxdownloadDataDirSize / 100;
-
- /** see {@link Environment#getExternalStorageDirectory()} */
- private final File mExternalStorageDir;
-
- /** see {@link Environment#getDownloadCacheDirectory()} */
- private final File mSystemCacheDir;
-
- /** The downloaded files are saved to this dir. it is the value returned by
- * {@link Context#getCacheDir()}.
- */
- private final File mDownloadDataDir;
-
- /** how often do we need to perform checks on space to make sure space is available */
- private static final int FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY = 1024 * 1024; // 1MB
- private int mBytesDownloadedSinceLastCheckOnSpace = 0;
-
- /** misc members */
- private final Context mContext;
-
- public StorageManager(Context context) {
- mContext = context;
- mDownloadDataDir = getDownloadDataDirectory(context);
- mExternalStorageDir = Environment.getExternalStorageDirectory();
- mSystemCacheDir = Environment.getDownloadCacheDirectory();
- startThreadToCleanupDatabaseAndPurgeFileSystem();
- }
-
- /** How often should database and filesystem be cleaned up to remove spurious files
- * from the file system and
- * The value is specified in terms of num of downloads since last time the cleanup was done.
- */
- private static final int FREQUENCY_OF_DATABASE_N_FILESYSTEM_CLEANUP = 250;
- private int mNumDownloadsSoFar = 0;
-
- synchronized void incrementNumDownloadsSoFar() {
- if (++mNumDownloadsSoFar % FREQUENCY_OF_DATABASE_N_FILESYSTEM_CLEANUP == 0) {
- startThreadToCleanupDatabaseAndPurgeFileSystem();
- }
- }
- /* start a thread to cleanup the following
- * remove spurious files from the file system
- * remove excess entries from the database
- */
- private Thread mCleanupThread = null;
- private synchronized void startThreadToCleanupDatabaseAndPurgeFileSystem() {
- if (mCleanupThread != null && mCleanupThread.isAlive()) {
- return;
- }
- mCleanupThread = new Thread() {
- @Override public void run() {
- removeSpuriousFiles();
- trimDatabase();
- }
- };
- mCleanupThread.start();
- }
-
- void verifySpaceBeforeWritingToFile(int destination, String path, long length)
- throws StopRequestException {
- // do this check only once for every 1MB of downloaded data
- if (incrementBytesDownloadedSinceLastCheckOnSpace(length) <
- FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY) {
- return;
- }
- verifySpace(destination, path, length);
- }
-
- void verifySpace(int destination, String path, long length) throws StopRequestException {
- resetBytesDownloadedSinceLastCheckOnSpace();
- File dir = null;
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "in verifySpace, destination: " + destination +
- ", path: " + path + ", length: " + length);
- }
- if (path == null) {
- throw new IllegalArgumentException("path can't be null");
- }
- switch (destination) {
- case Downloads.Impl.DESTINATION_CACHE_PARTITION:
- case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
- case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
- dir = mDownloadDataDir;
- break;
- case Downloads.Impl.DESTINATION_EXTERNAL:
- dir = mExternalStorageDir;
- break;
- case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
- dir = mSystemCacheDir;
- break;
- case Downloads.Impl.DESTINATION_FILE_URI:
- if (path.startsWith(mExternalStorageDir.getPath())) {
- dir = mExternalStorageDir;
- } else if (path.startsWith(mDownloadDataDir.getPath())) {
- dir = mDownloadDataDir;
- } else if (path.startsWith(mSystemCacheDir.getPath())) {
- dir = mSystemCacheDir;
- }
- break;
- }
- if (dir == null) {
- throw new IllegalStateException("invalid combination of destination: " + destination +
- ", path: " + path);
- }
- findSpace(dir, length, destination);
- }
-
- /**
- * finds space in the given filesystem (input param: root) to accommodate # of bytes
- * specified by the input param(targetBytes).
- * returns true if found. false otherwise.
- */
- private synchronized void findSpace(File root, long targetBytes, int destination)
- throws StopRequestException {
- if (targetBytes == 0) {
- return;
- }
- if (destination == Downloads.Impl.DESTINATION_FILE_URI ||
- destination == Downloads.Impl.DESTINATION_EXTERNAL) {
- if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- throw new StopRequestException(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted");
- }
- }
- // is there enough space in the file system of the given param 'root'.
- long bytesAvailable = getAvailableBytesInFileSystemAtGivenRoot(root);
- if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
- /* filesystem's available space is below threshold for low space warning.
- * threshold typically is 10% of download data dir space quota.
- * try to cleanup and see if the low space situation goes away.
- */
- discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold);
- removeSpuriousFiles();
- bytesAvailable = getAvailableBytesInFileSystemAtGivenRoot(root);
- if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
- /*
- * available space is still below the threshold limit.
- *
- * If this is system cache dir, print a warning.
- * otherwise, don't allow downloading until more space
- * is available because downloadmanager shouldn't end up taking those last
- * few MB of space left on the filesystem.
- */
- if (root.equals(mSystemCacheDir)) {
- Log.w(Constants.TAG, "System cache dir ('/cache') is running low on space." +
- "space available (in bytes): " + bytesAvailable);
- } else {
- throw new StopRequestException(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "space in the filesystem rooted at: " + root +
- " is below 10% availability. stopping this download.");
- }
- }
- }
- if (root.equals(mDownloadDataDir)) {
- // this download is going into downloads data dir. check space in that specific dir.
- bytesAvailable = getAvailableBytesInDownloadsDataDir(mDownloadDataDir);
- if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
- // print a warning
- Log.w(Constants.TAG, "Downloads data dir: " + root +
- " is running low on space. space available (in bytes): " + bytesAvailable);
- }
- if (bytesAvailable < targetBytes) {
- // Insufficient space; make space.
- discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold);
- removeSpuriousFiles();
- bytesAvailable = getAvailableBytesInDownloadsDataDir(mDownloadDataDir);
- }
- }
- if (bytesAvailable < targetBytes) {
- throw new StopRequestException(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "not enough free space in the filesystem rooted at: " + root +
- " and unable to free any more");
- }
- }
-
- /**
- * returns the number of bytes available in the downloads data dir
- * TODO this implementation is too slow. optimize it.
- */
- private long getAvailableBytesInDownloadsDataDir(File root) {
- File[] files = root.listFiles();
- long space = sMaxdownloadDataDirSize;
- if (files == null) {
- return space;
- }
- int size = files.length;
- for (int i = 0; i < size; i++) {
- space -= files[i].length();
- }
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "available space (in bytes) in downloads data dir: " + space);
- }
- return space;
- }
-
- private long getAvailableBytesInFileSystemAtGivenRoot(File root) {
- StatFs stat = new StatFs(root.getPath());
- // put a bit of margin (in case creating the file grows the system by a few blocks)
- long availableBlocks = (long) stat.getAvailableBlocks() - 4;
- long size = stat.getBlockSize() * availableBlocks;
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "available space (in bytes) in filesystem rooted at: " +
- root.getPath() + " is: " + size);
- }
- return size;
- }
-
- File locateDestinationDirectory(String mimeType, int destination, long contentLength)
- throws StopRequestException {
- switch (destination) {
- case Downloads.Impl.DESTINATION_CACHE_PARTITION:
- case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
- case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
- return mDownloadDataDir;
- case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
- return mSystemCacheDir;
- case Downloads.Impl.DESTINATION_EXTERNAL:
- File base = new File(mExternalStorageDir.getPath() + Constants.DEFAULT_DL_SUBDIR);
- if (!base.isDirectory() && !base.mkdir()) {
- // Can't create download directory, e.g. because a file called "download"
- // already exists at the root level, or the SD card filesystem is read-only.
- throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
- "unable to create external downloads directory " + base.getPath());
- }
- return base;
- default:
- throw new IllegalStateException("unexpected value for destination: " + destination);
- }
- }
-
- File getDownloadDataDirectory() {
- return mDownloadDataDir;
- }
-
- public static File getDownloadDataDirectory(Context context) {
- return context.getCacheDir();
- }
-
- /**
- * Deletes purgeable files from the cache partition. This also deletes
- * the matching database entries. Files are deleted in LRU order until
- * the total byte size is greater than targetBytes
- */
- private long discardPurgeableFiles(int destination, long targetBytes) {
- if (true || Constants.LOGV) {
- Log.i(Constants.TAG, "discardPurgeableFiles: destination = " + destination +
- ", targetBytes = " + targetBytes);
- }
- String destStr = (destination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) ?
- String.valueOf(destination) :
- String.valueOf(Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
- String[] bindArgs = new String[]{destStr};
- Cursor cursor = mContext.getContentResolver().query(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- null,
- "( " +
- Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
- Downloads.Impl.COLUMN_DESTINATION + " = ? )",
- bindArgs,
- Downloads.Impl.COLUMN_LAST_MODIFICATION);
- if (cursor == null) {
- return 0;
- }
- long totalFreed = 0;
- try {
- final int dataIndex = cursor.getColumnIndex(Downloads.Impl._DATA);
- while (cursor.moveToNext() && totalFreed < targetBytes) {
- final String data = cursor.getString(dataIndex);
- if (TextUtils.isEmpty(data)) continue;
-
- File file = new File(data);
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "purging " + file.getAbsolutePath() + " for "
- + file.length() + " bytes");
- }
- totalFreed += file.length();
- file.delete();
- long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
- mContext.getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
- null, null);
- }
- } finally {
- cursor.close();
- }
- if (true || Constants.LOGV) {
- Log.i(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
- targetBytes + " requested");
- }
- return totalFreed;
- }
-
- /**
- * Removes files in the systemcache and downloads data dir without corresponding entries in
- * the downloads database.
- * This can occur if a delete is done on the database but the file is not removed from the
- * filesystem (due to sudden death of the process, for example).
- * This is not a very common occurrence. So, do this only once in a while.
- */
- private void removeSpuriousFiles() {
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "in removeSpuriousFiles");
- }
- // get a list of all files in system cache dir and downloads data dir
- List<File> files = new ArrayList<File>();
- File[] listOfFiles = mSystemCacheDir.listFiles();
- if (listOfFiles != null) {
- files.addAll(Arrays.asList(listOfFiles));
- }
- listOfFiles = mDownloadDataDir.listFiles();
- if (listOfFiles != null) {
- files.addAll(Arrays.asList(listOfFiles));
- }
- if (files.size() == 0) {
- return;
- }
- Cursor cursor = mContext.getContentResolver().query(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- new String[] { Downloads.Impl._DATA }, null, null, null);
- try {
- if (cursor != null) {
- while (cursor.moveToNext()) {
- String filename = cursor.getString(0);
- if (!TextUtils.isEmpty(filename)) {
- if (LOGV) {
- Log.i(Constants.TAG, "in removeSpuriousFiles, preserving file " +
- filename);
- }
- files.remove(new File(filename));
- }
- }
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- // delete files owned by us, but that don't appear in our database
- final int myUid = android.os.Process.myUid();
- for (File file : files) {
- final String path = file.getAbsolutePath();
- try {
- final StructStat stat = Os.stat(path);
- if (stat.st_uid == myUid) {
- if (Constants.LOGVV) {
- Log.d(TAG, "deleting spurious file " + path);
- }
- file.delete();
- }
- } catch (ErrnoException e) {
- Log.w(TAG, "stat(" + path + ") result: " + e);
- }
- }
- }
-
- /**
- * Drops old rows from the database to prevent it from growing too large
- * TODO logic in this method needs to be optimized. maintain the number of downloads
- * in memory - so that this method can limit the amount of data read.
- */
- private void trimDatabase() {
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "in trimDatabase");
- }
- Cursor cursor = null;
- try {
- cursor = mContext.getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- new String[] { Downloads.Impl._ID },
- Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
- Downloads.Impl.COLUMN_LAST_MODIFICATION);
- if (cursor == null) {
- // This isn't good - if we can't do basic queries in our database,
- // nothing's gonna work
- Log.e(Constants.TAG, "null cursor in trimDatabase");
- return;
- }
- if (cursor.moveToFirst()) {
- int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
- int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
- while (numDelete > 0) {
- Uri downloadUri = ContentUris.withAppendedId(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
- mContext.getContentResolver().delete(downloadUri, null, null);
- if (!cursor.moveToNext()) {
- break;
- }
- numDelete--;
- }
- }
- } catch (SQLiteException e) {
- // trimming the database raised an exception. alright, ignore the exception
- // and return silently. trimming database is not exactly a critical operation
- // and there is no need to propagate the exception.
- Log.w(Constants.TAG, "trimDatabase failed with exception: " + e.getMessage());
- return;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
-
- private synchronized int incrementBytesDownloadedSinceLastCheckOnSpace(long val) {
- mBytesDownloadedSinceLastCheckOnSpace += val;
- return mBytesDownloadedSinceLastCheckOnSpace;
- }
-
- private synchronized void resetBytesDownloadedSinceLastCheckOnSpace() {
- mBytesDownloadedSinceLastCheckOnSpace = 0;
- }
-}