summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVasu Nori <vnori@google.com>2010-12-23 16:47:13 -0800
committerAndroid (Google) Code Review <android-gerrit@google.com>2010-12-23 16:47:13 -0800
commitf2053e4ed8a19c4bf331320028ac8bdb997e1e89 (patch)
tree0e12c40df93327bc9a33e532d397ee71817be573
parentdc869afe6841a03b5fcc4d19127d82b317d69cde (diff)
parent5218d33d57990c3e3549c58bd3f0ac244dfc3d59 (diff)
downloadandroid_packages_providers_DownloadProvider-f2053e4ed8a19c4bf331320028ac8bdb997e1e89.zip
android_packages_providers_DownloadProvider-f2053e4ed8a19c4bf331320028ac8bdb997e1e89.tar.gz
android_packages_providers_DownloadProvider-f2053e4ed8a19c4bf331320028ac8bdb997e1e89.tar.bz2
Merge "bug:3286430 set quota on downloads data dir"
-rw-r--r--src/com/android/providers/downloads/Constants.java3
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java6
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java6
-rw-r--r--src/com/android/providers/downloads/DownloadService.java114
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java202
-rw-r--r--src/com/android/providers/downloads/Helpers.java224
-rw-r--r--src/com/android/providers/downloads/StopRequestException.java37
-rw-r--r--src/com/android/providers/downloads/StorageManager.java452
-rw-r--r--tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java2
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java5
10 files changed, 624 insertions, 427 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index da2c2fd..6419a5e 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -16,6 +16,7 @@
package com.android.providers.downloads;
+import android.os.Environment;
import android.util.Log;
/**
@@ -79,7 +80,7 @@ public class Constants {
public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
/** Where we store downloaded files on the external storage */
- public static final String DEFAULT_DL_SUBDIR = "/download";
+ public static final String DEFAULT_DL_SUBDIR = "/" + Environment.DIRECTORY_DOWNLOADS;
/** A magic filename that is allowed to exist within the system cache */
public static final String KNOWN_SPURIOUS_FILENAME = "lost+found";
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index d1ea43e..ca43ea9 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -22,7 +22,6 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
-import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.drm.mobile1.DrmRawContent;
import android.net.ConnectivityManager;
@@ -427,7 +426,7 @@ public class DownloadInfo {
return NETWORK_OK;
}
- void startIfReady(long now) {
+ void startIfReady(long now, StorageManager storageManager) {
if (!isReadyToStart(now)) {
return;
}
@@ -444,7 +443,8 @@ public class DownloadInfo {
values.put(Impl.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
}
- DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
+ DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this,
+ storageManager);
mHasActiveThread = true;
mSystemFacade.startThread(downloader);
}
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index d848b65..a995253 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -422,7 +422,11 @@ public final class DownloadProvider extends ContentProvider {
if (appInfo != null) {
mDefContainerUid = appInfo.uid;
}
- mDownloadsDataDir = Helpers.getDownloadsDataDirectory(getContext());
+ // start the DownloadService class. don't wait for the 1st download to be issued.
+ // saves us by getting some initialization code in DownloadService out of the way.
+ Context context = getContext();
+ context.startService(new Intent(context, DownloadService.class));
+ mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory();
return true;
}
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 62e355c..bc4083c 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -16,23 +16,23 @@
package com.android.providers.downloads;
+import com.google.android.collect.Maps;
+import com.google.common.annotations.VisibleForTesting;
+
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.ContentObserver;
import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
import android.media.IMediaScannerListener;
import android.media.IMediaScannerService;
import android.net.Uri;
-import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
@@ -41,12 +41,8 @@ import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
-import com.google.android.collect.Maps;
-import com.google.common.annotations.VisibleForTesting;
-
import java.io.File;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@@ -93,18 +89,16 @@ public class DownloadService extends Service {
private boolean mMediaScannerConnecting;
- private static final int LOCATION_SYSTEM_CACHE = 1;
- private static final int LOCATION_DOWNLOAD_DATA_DIR = 2;
-
/**
* The IPC interface to the Media Scanner
*/
private IMediaScannerService mMediaScannerService;
- private File mDownloadsDataDir;
@VisibleForTesting
SystemFacade mSystemFacade;
+ private StorageManager mStorageManager;
+
/**
* Receives notifications when the data in the content provider changes
*/
@@ -222,7 +216,7 @@ public class DownloadService extends Service {
mNotifier = new DownloadNotification(this, mSystemFacade);
mSystemFacade.cancelAllNotifications();
- mDownloadsDataDir = Helpers.getDownloadsDataDirectory(getApplicationContext());
+ mStorageManager = StorageManager.getInstance(getApplicationContext());
updateFromProvider();
}
@@ -269,13 +263,6 @@ public class DownloadService extends Service {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
- trimDatabase();
- // remove spurious files from system cache
- removeSpuriousFiles(LOCATION_SYSTEM_CACHE);
- // remove spurious files from downloads dir
- removeSpuriousFiles(LOCATION_DOWNLOAD_DATA_DIR);
-
boolean keepService = false;
// for each update from the database, remember which download is
// supposed to get restarted soonest in the future
@@ -430,91 +417,6 @@ public class DownloadService extends Service {
}
/**
- * Removes files that may have been left behind in the systemcache or
- * /data/downloads directory
- */
- private void removeSpuriousFiles(int location) {
- File base = (location == LOCATION_SYSTEM_CACHE) ?
- Environment.getDownloadCacheDirectory() : mDownloadsDataDir;
- File[] files = base.listFiles();
- if (files == null) {
- // The cache folder doesn't appear to exist (this is likely the case
- // when running the simulator).
- return;
- }
- HashSet<String> fileSet = new HashSet<String>();
- for (int i = 0; i < files.length; i++) {
- if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
- continue;
- }
- if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
- continue;
- }
- fileSet.add(files[i].getPath());
- }
-
- Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- new String[] { Downloads.Impl._DATA }, null, null, null);
- if (cursor != null) {
- if (cursor.moveToFirst()) {
- do {
- fileSet.remove(cursor.getString(0));
- } while (cursor.moveToNext());
- }
- cursor.close();
- }
- Iterator<String> iterator = fileSet.iterator();
- while (iterator.hasNext()) {
- String filename = iterator.next();
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "deleting spurious file " + filename);
- }
- new File(filename).delete();
- }
- }
-
- /**
- * Drops old rows from the database to prevent it from growing too large
- */
- private void trimDatabase() {
- Cursor cursor = null;
- try {
- cursor = 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));
- 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();
- }
- }
- }
-
- /**
* Keeps a local copy of the info about a download, and initiates the
* download if appropriate.
*/
@@ -526,7 +428,7 @@ public class DownloadService extends Service {
info.logVerboseInfo();
}
- info.startIfReady(now);
+ info.startIfReady(now, mStorageManager);
return info;
}
@@ -550,7 +452,7 @@ public class DownloadService extends Service {
mSystemFacade.cancelNotification(info.mId);
}
- info.startIfReady(now);
+ info.startIfReady(now, mStorageManager);
}
/**
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index c497d5c..fefbe1d 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -52,14 +52,17 @@ import java.util.Locale;
*/
public class DownloadThread extends Thread {
- private Context mContext;
- private DownloadInfo mInfo;
- private SystemFacade mSystemFacade;
+ private final Context mContext;
+ private final DownloadInfo mInfo;
+ private final SystemFacade mSystemFacade;
+ private final StorageManager mStorageManager;
- public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
+ public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
+ StorageManager storageManager) {
mContext = context;
mSystemFacade = systemFacade;
mInfo = info;
+ mStorageManager = storageManager;
}
/**
@@ -78,7 +81,7 @@ public class DownloadThread extends Thread {
/**
* State for the entire run() method.
*/
- private static class State {
+ static class State {
public String mFilename;
public FileOutputStream mStream;
public String mMimeType;
@@ -112,28 +115,6 @@ public class DownloadThread extends Thread {
}
/**
- * Raised from methods called by run() to indicate that the current request should be stopped
- * immediately.
- *
- * Note the message passed to this exception will be logged and therefore must be guaranteed
- * not to contain any PII, meaning it generally can't include any information about the request
- * URI, headers, or destination filename.
- */
- private class StopRequest extends Throwable {
- public int mFinalStatus;
-
- public StopRequest(int finalStatus, String message) {
- super(message);
- mFinalStatus = finalStatus;
- }
-
- public StopRequest(int finalStatus, String message, Throwable throwable) {
- super(message, throwable);
- mFinalStatus = finalStatus;
- }
- }
-
- /**
* Raised from methods called by executeDownload() to indicate that the download should be
* retried immediately.
*/
@@ -142,6 +123,7 @@ public class DownloadThread extends Thread {
/**
* Executes the download in a separate thread
*/
+ @Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
@@ -187,16 +169,22 @@ public class DownloadThread extends Thread {
}
finalizeDestinationFile(state);
finalStatus = Downloads.Impl.STATUS_SUCCESS;
- } catch (StopRequest error) {
+ } catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
errorMsg = "Aborting request for download " + mInfo.mId + ": " + error.getMessage();
Log.w(Constants.TAG, errorMsg);
+ if (Constants.LOGV) {
+ Log.w(Constants.TAG, errorMsg, error);
+ }
finalStatus = error.mFinalStatus;
// fall through to finally block
} catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
errorMsg = "Exception for id " + mInfo.mId + ": " + ex.getMessage();
- Log.w(Constants.TAG, "Exception for id " + mInfo.mId + ": " + ex);
+ Log.w(Constants.TAG, errorMsg);
finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ if (Constants.LOGV) {
+ Log.w(Constants.TAG, errorMsg, ex);
+ }
// falls through to the code that reports an error
} finally {
if (wakeLock != null) {
@@ -213,6 +201,7 @@ public class DownloadThread extends Thread {
state.mNewUri, state.mMimeType, errorMsg);
mInfo.mHasActiveThread = false;
}
+ mStorageManager.incrementNumDownloadsSoFar();
}
/**
@@ -220,7 +209,7 @@ public class DownloadThread extends Thread {
* and transfer the data to the destination file.
*/
private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
- throws StopRequest, RetryDownload {
+ throws StopRequestException, RetryDownload {
InnerState innerState = new InnerState();
byte data[] = new byte[Constants.BUFFER_SIZE];
@@ -245,7 +234,7 @@ public class DownloadThread extends Thread {
/**
* Check if current connectivity is valid for this request.
*/
- private void checkConnectivity(State state) throws StopRequest {
+ private void checkConnectivity(State state) throws StopRequestException {
int networkUsable = mInfo.checkCanUseNetwork();
if (networkUsable != DownloadInfo.NETWORK_OK) {
int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
@@ -256,7 +245,8 @@ public class DownloadThread extends Thread {
status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
mInfo.notifyPauseDueToSize(false);
}
- throw new StopRequest(status, mInfo.getLogMessageForNetworkError(networkUsable));
+ throw new StopRequestException(status,
+ mInfo.getLogMessageForNetworkError(networkUsable));
}
}
@@ -266,7 +256,7 @@ public class DownloadThread extends Thread {
* @param entityStream stream for reading the HTTP response entity
*/
private void transferData(State state, InnerState innerState, byte[] data,
- InputStream entityStream) throws StopRequest {
+ InputStream entityStream) throws StopRequestException {
for (;;) {
int bytesRead = readFromResponse(state, innerState, data, entityStream);
if (bytesRead == -1) { // success, end of stream already reached
@@ -291,7 +281,7 @@ public class DownloadThread extends Thread {
/**
* Called after a successful completion to take any necessary action on the downloaded file.
*/
- private void finalizeDestinationFile(State state) throws StopRequest {
+ private void finalizeDestinationFile(State state) throws StopRequestException {
if (isDrmFile(state)) {
transferToDrm(state);
} else {
@@ -352,13 +342,13 @@ public class DownloadThread extends Thread {
/**
* Transfer the downloaded destination file to the DRM store.
*/
- private void transferToDrm(State state) throws StopRequest {
+ private void transferToDrm(State state) throws StopRequestException {
File file = new File(state.mFilename);
Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
file.delete();
if (item == null) {
- throw new StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_UNKNOWN_ERROR,
"unable to add file to DrmProvider");
} else {
state.mFilename = item.getDataString();
@@ -388,15 +378,15 @@ public class DownloadThread extends Thread {
* Check if the download has been paused or canceled, stopping the request appropriately if it
* has been.
*/
- private void checkPausedOrCanceled(State state) throws StopRequest {
+ private void checkPausedOrCanceled(State state) throws StopRequestException {
synchronized (mInfo) {
if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
- throw new StopRequest(Downloads.Impl.STATUS_PAUSED_BY_APP,
+ throw new StopRequestException(Downloads.Impl.STATUS_PAUSED_BY_APP,
"download paused by owner");
}
}
if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
- throw new StopRequest(Downloads.Impl.STATUS_CANCELED, "download canceled");
+ throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
}
}
@@ -423,12 +413,13 @@ public class DownloadThread extends Thread {
* @param bytesRead how many bytes to write from the buffer
*/
private void writeDataToDestination(State state, byte[] data, int bytesRead)
- throws StopRequest {
+ throws StopRequestException {
for (;;) {
try {
if (state.mStream == null) {
state.mStream = new FileOutputStream(state.mFilename, true);
}
+ mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, null, bytesRead);
state.mStream.write(data, 0, bytesRead);
if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
&& !isDrmFile(state)) {
@@ -436,24 +427,7 @@ public class DownloadThread extends Thread {
}
return;
} catch (IOException ex) {
- if (mInfo.isOnCache()) {
- if (Helpers.discardPurgeableFiles(mInfo.mDestination, mContext,
- Constants.BUFFER_SIZE)) {
- continue;
- }
- } else if (!Helpers.isExternalMediaMounted()) {
- throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted while writing destination file");
- }
-
- long availableBytes =
- Helpers.getAvailableBytes(Helpers.getFilesystemRoot(mContext, state.mFilename));
- if (availableBytes < bytesRead) {
- throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "insufficient space while writing destination file", ex);
- }
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
- "while writing destination file: " + ex.toString(), ex);
+ mStorageManager.verifySpace(mInfo.mDestination, null, bytesRead);
}
}
}
@@ -462,7 +436,7 @@ public class DownloadThread extends Thread {
* Called when we've reached the end of the HTTP response stream, to update the database and
* check for consistency.
*/
- private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
+ private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
if (innerState.mHeaderContentLength == null) {
@@ -474,10 +448,10 @@ public class DownloadThread extends Thread {
&& (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
if (lengthMismatched) {
if (cannotResume(innerState)) {
- throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
+ throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
"mismatched content length");
} else {
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"closed socket before end of file");
}
}
@@ -494,7 +468,7 @@ public class DownloadThread extends Thread {
* @return the number of bytes actually read or -1 if the end of the stream has been reached
*/
private int readFromResponse(State state, InnerState innerState, byte[] data,
- InputStream entityStream) throws StopRequest {
+ InputStream entityStream) throws StopRequestException {
try {
return entityStream.read(data);
} catch (IOException ex) {
@@ -505,10 +479,10 @@ public class DownloadThread extends Thread {
if (cannotResume(innerState)) {
String message = "while reading response: " + ex.toString()
+ ", can't resume interrupted download with no ETag";
- throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
+ throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
message, ex);
} else {
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"while reading response: " + ex.toString(), ex);
}
}
@@ -519,12 +493,12 @@ public class DownloadThread extends Thread {
* @return an InputStream to read the response entity
*/
private InputStream openResponseEntity(State state, HttpResponse response)
- throws StopRequest {
+ throws StopRequestException {
try {
return response.getEntity().getContent();
} catch (IOException ex) {
logNetworkState();
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"while getting entity: " + ex.toString(), ex);
}
}
@@ -541,7 +515,7 @@ public class DownloadThread extends Thread {
* file and updating the database.
*/
private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
- throws StopRequest {
+ throws StopRequestException {
if (innerState.mContinuingDownload) {
// ignore response headers on resume requests
return;
@@ -549,25 +523,21 @@ public class DownloadThread extends Thread {
readResponseHeaders(state, innerState, response);
- try {
- state.mFilename = Helpers.generateSaveFile(
- mContext,
- mInfo.mUri,
- mInfo.mHint,
- innerState.mHeaderContentDisposition,
- innerState.mHeaderContentLocation,
- state.mMimeType,
- mInfo.mDestination,
- (innerState.mHeaderContentLength != null) ?
- Long.parseLong(innerState.mHeaderContentLength) : 0,
- mInfo.mIsPublicApi);
- } catch (Helpers.GenerateSaveFileError exc) {
- throw new StopRequest(exc.mStatus, exc.mMessage);
- }
+ state.mFilename = Helpers.generateSaveFile(
+ mContext,
+ mInfo.mUri,
+ mInfo.mHint,
+ innerState.mHeaderContentDisposition,
+ innerState.mHeaderContentLocation,
+ state.mMimeType,
+ mInfo.mDestination,
+ (innerState.mHeaderContentLength != null) ?
+ Long.parseLong(innerState.mHeaderContentLength) : 0,
+ mInfo.mIsPublicApi, mStorageManager);
try {
state.mStream = new FileOutputStream(state.mFilename);
} catch (FileNotFoundException exc) {
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"while opening destination file: " + exc.toString(), exc);
}
if (Constants.LOGV) {
@@ -600,7 +570,7 @@ public class DownloadThread extends Thread {
* Read headers from the HTTP response and store them into local state.
*/
private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
- throws StopRequest {
+ throws StopRequestException {
Header header = response.getFirstHeader("Content-Disposition");
if (header != null) {
innerState.mHeaderContentDisposition = header.getValue();
@@ -651,7 +621,7 @@ public class DownloadThread extends Thread {
&& (headerTransferEncoding == null
|| !headerTransferEncoding.equalsIgnoreCase("chunked"));
if (!mInfo.mNoIntegrity && noSizeInfo) {
- throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
"can't know size of download, giving up");
}
}
@@ -660,7 +630,7 @@ public class DownloadThread extends Thread {
* Check the HTTP response status and handle anything unusual (e.g. not 200/206).
*/
private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
- throws StopRequest, RetryDownload {
+ throws StopRequestException, RetryDownload {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
handleServiceUnavailable(state, response);
@@ -669,6 +639,10 @@ public class DownloadThread extends Thread {
handleRedirect(state, response, statusCode);
}
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "recevd_status = " + statusCode +
+ ", mContinuingDownload = " + innerState.mContinuingDownload);
+ }
int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
if (statusCode != expectedStatus) {
handleOtherStatus(state, innerState, statusCode);
@@ -679,7 +653,7 @@ public class DownloadThread extends Thread {
* Handle a status that we don't know how to deal with properly.
*/
private void handleOtherStatus(State state, InnerState innerState, int statusCode)
- throws StopRequest {
+ throws StopRequestException {
int finalStatus;
if (Downloads.Impl.isStatusError(statusCode)) {
finalStatus = statusCode;
@@ -690,20 +664,21 @@ public class DownloadThread extends Thread {
} else {
finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
}
- throw new StopRequest(finalStatus, "http error " + statusCode + ", mContinuingDownload: " +
- innerState.mContinuingDownload);
+ throw new StopRequestException(finalStatus, "http error " +
+ statusCode + ", mContinuingDownload: " + innerState.mContinuingDownload);
}
/**
* Handle a 3xx redirect status.
*/
private void handleRedirect(State state, HttpResponse response, int statusCode)
- throws StopRequest, RetryDownload {
+ throws StopRequestException, RetryDownload {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
}
if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
- throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
+ throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
+ "too many redirects");
}
Header header = response.getFirstHeader("Location");
if (header == null) {
@@ -721,7 +696,7 @@ public class DownloadThread extends Thread {
Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
+ " for " + mInfo.mUri);
}
- throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
"Couldn't resolve redirect URI");
}
++state.mRedirectCount;
@@ -736,7 +711,8 @@ public class DownloadThread extends Thread {
/**
* Handle a 503 Service Unavailable status by processing the Retry-After header.
*/
- private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
+ private void handleServiceUnavailable(State state, HttpResponse response)
+ throws StopRequestException {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP response code 503");
}
@@ -763,7 +739,7 @@ public class DownloadThread extends Thread {
// ignored - retryAfter stays 0 in this case.
}
}
- throw new StopRequest(Downloads.Impl.STATUS_WAITING_TO_RETRY,
+ throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
"got 503 Service Unavailable, will retry later");
}
@@ -771,15 +747,15 @@ public class DownloadThread extends Thread {
* Send the request to the server, handling any I/O exceptions.
*/
private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
- throws StopRequest {
+ throws StopRequestException {
try {
return client.execute(request);
} catch (IllegalArgumentException ex) {
- throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
"while trying to execute request: " + ex.toString(), ex);
} catch (IOException ex) {
logNetworkState();
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"while trying to execute request: " + ex.toString(), ex);
}
}
@@ -801,33 +777,49 @@ public class DownloadThread extends Thread {
* appropriately for resumption.
*/
private void setupDestinationFile(State state, InnerState innerState)
- throws StopRequest {
+ throws StopRequestException {
if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
- if (!Helpers.isFilenameValid(state.mFilename,
- Helpers.getDownloadsDataDirectory(mContext))) {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
+ ", and state.mFilename: " + state.mFilename);
+ }
+ if (!Helpers.isFilenameValid(state.mFilename,
+ mStorageManager.getDownloadDataDirectory())) {
// this should never happen
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File f = new File(state.mFilename);
if (f.exists()) {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", and state.mFilename: " + state.mFilename);
+ }
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from scratch
f.delete();
state.mFilename = null;
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", BUT starting from scratch again: ");
+ }
} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
// This should've been caught upon failure
f.delete();
- throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
+ throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", and starting with file of length: " + fileLength);
+ }
try {
state.mStream = new FileOutputStream(state.mFilename, true);
} catch (FileNotFoundException exc) {
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"while opening destination for resuming: " + exc.toString(), exc);
}
innerState.mBytesSoFar = (int) fileLength;
@@ -836,6 +828,10 @@ public class DownloadThread extends Thread {
}
innerState.mHeaderETag = mInfo.mETag;
innerState.mContinuingDownload = true;
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", and setting mContinuingDownload to true: ");
+ }
}
}
}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index f392f3e..a9c48be 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,16 +16,13 @@
package com.android.providers.downloads;
-import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.database.Cursor;
import android.drm.mobile1.DrmRawContent;
import android.net.Uri;
import android.os.Environment;
-import android.os.StatFs;
import android.os.SystemClock;
import android.provider.Downloads;
import android.util.Config;
@@ -42,7 +39,6 @@ import java.util.regex.Pattern;
* Some helper functions for the download manager
*/
public class Helpers {
-
public static Random sRandom = new Random(SystemClock.uptimeMillis());
/** Regex used to parse content-disposition headers */
@@ -71,22 +67,9 @@ public class Helpers {
}
/**
- * Exception thrown from methods called by generateSaveFile() for any fatal error.
- */
- public static class GenerateSaveFileError extends Exception {
- int mStatus;
- String mMessage;
-
- public GenerateSaveFileError(int status, String message) {
- mStatus = status;
- mMessage = message;
- }
- }
-
- /**
* Creates a filename (where the file should be saved) from info about a download.
*/
- public static String generateSaveFile(
+ static String generateSaveFile(
Context context,
String url,
String hint,
@@ -95,64 +78,24 @@ public class Helpers {
String mimeType,
int destination,
long contentLength,
- boolean isPublicApi) throws GenerateSaveFileError {
+ boolean isPublicApi, StorageManager storageManager) throws StopRequestException {
checkCanHandleDownload(context, mimeType, destination, isPublicApi);
+ String path;
+ File base = null;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
- String path = verifyFileUri(context, hint, contentLength);
- String c = getFullPath(path, mimeType, destination, null);
- return c;
+ path = Uri.parse(hint).getPath();
} else {
- return chooseFullPath(context, url, hint, contentDisposition, contentLocation, mimeType,
- destination, contentLength);
+ base = storageManager.locateDestinationDirectory(mimeType, destination,
+ contentLength);
+ path = chooseFilename(url, hint, contentDisposition, contentLocation,
+ destination);
}
- }
-
- private static String verifyFileUri(Context context, String hint, long contentLength)
- throws GenerateSaveFileError {
- if (!isExternalMediaMounted()) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted");
- }
- String path = Uri.parse(hint).getPath();
- if (getAvailableBytes(getFilesystemRoot(context, path)) < contentLength) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "insufficient space on external storage");
- }
-
- return path;
- }
-
- /**
- * @return the root of the filesystem containing the given path
- */
- static File getFilesystemRoot(Context context, String path) {
- File cache = Environment.getDownloadCacheDirectory();
- if (path.startsWith(cache.getPath())) {
- return cache;
- }
- File systemCache = Helpers.getDownloadsDataDirectory(context);
- if (path.startsWith(systemCache.getPath())) {
- return systemCache;
- }
- File external = Environment.getExternalStorageDirectory();
- if (path.startsWith(external.getPath())) {
- return external;
- }
- throw new IllegalArgumentException("Cannot determine filesystem root for " + path);
- }
-
- private static String chooseFullPath(Context context, String url, String hint,
- String contentDisposition, String contentLocation,
- String mimeType, int destination, long contentLength)
- throws GenerateSaveFileError {
- File base = locateDestinationDirectory(context, mimeType, destination, contentLength);
- String filename = chooseFilename(url, hint, contentDisposition, contentLocation,
- destination);
- return getFullPath(filename, mimeType, destination, base);
+ storageManager.verifySpace(destination, path, contentLength);
+ return getFullPath(path, mimeType, destination, base);
}
private static String getFullPath(String filename, String mimeType, int destination,
- File base) throws GenerateSaveFileError {
+ File base) throws StopRequestException {
// Split filename between base and extension
// Add an extension if filename does not have one
String extension = null;
@@ -178,7 +121,7 @@ public class Helpers {
}
private static void checkCanHandleDownload(Context context, String mimeType, int destination,
- boolean isPublicApi) throws GenerateSaveFileError {
+ boolean isPublicApi) throws StopRequestException {
if (isPublicApi) {
return;
}
@@ -186,7 +129,7 @@ public class Helpers {
if (destination == Downloads.Impl.DESTINATION_EXTERNAL
|| destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
if (mimeType == null) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
+ throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
"external download with no mime type not allowed");
}
if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
@@ -211,92 +154,13 @@ public class Helpers {
if (Constants.LOGV) {
Log.v(Constants.TAG, "no handler found for type " + mimeType);
}
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
+ throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
"no handler found for this download type");
}
}
}
}
- private static File locateDestinationDirectory(Context context, String mimeType,
- int destination, long contentLength)
- throws GenerateSaveFileError {
- // DRM messages should be temporarily stored internally and then passed to
- // the DRM content provider
- if (destination == Downloads.Impl.DESTINATION_CACHE_PARTITION
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
- || destination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION
- || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
- return getCacheDestination(context, contentLength, destination);
- }
-
- return getExternalDestination(contentLength);
- }
-
- private static File getExternalDestination(long contentLength) throws GenerateSaveFileError {
- if (!isExternalMediaMounted()) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted");
- }
-
- File root = Environment.getExternalStorageDirectory();
- if (getAvailableBytes(root) < contentLength) {
- // Insufficient space.
- Log.d(Constants.TAG, "download aborted - not enough free space");
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "insufficient space on external media");
- }
-
- File base = new File(root.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 GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR,
- "unable to create external downloads directory " + base.getPath());
- }
- return base;
- }
-
- public static boolean isExternalMediaMounted() {
- if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- // No SD card found.
- Log.d(Constants.TAG, "no external storage");
- return false;
- }
- return true;
- }
-
- private static File getCacheDestination(Context context, long contentLength, int destination)
- throws GenerateSaveFileError {
- File base;
- base = (destination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) ?
- Environment.getDownloadCacheDirectory() :
- Helpers.getDownloadsDataDirectory(context);
- long bytesAvailable = getAvailableBytes(base);
- while (bytesAvailable < contentLength) {
- // Insufficient space; try discarding purgeable files.
- if (!discardPurgeableFiles(destination, context, contentLength - bytesAvailable)) {
- // No files to purge, give up.
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "not enough free space in internal download storage: " + base +
- ", unable to free any more");
- }
- bytesAvailable = getAvailableBytes(base);
- }
- return base;
- }
-
- /**
- * @return the number of bytes available on the filesystem rooted at the given File
- */
- public static long getAvailableBytes(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;
- return stat.getBlockSize() * availableBlocks;
- }
-
private static String chooseFilename(String url, String hint, String contentDisposition,
String contentLocation, int destination) {
String filename = null;
@@ -445,7 +309,7 @@ public class Helpers {
}
private static String chooseUniqueFilename(int destination, String filename,
- String extension, boolean recoveryDir) throws GenerateSaveFileError {
+ String extension, boolean recoveryDir) throws StopRequestException {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
&& (!recoveryDir ||
@@ -483,62 +347,11 @@ public class Helpers {
sequence += sRandom.nextInt(magnitude) + 1;
}
}
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"failed to generate an unused filename on internal download storage");
}
/**
- * 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.
- */
- static final boolean discardPurgeableFiles(int destination, Context context,
- long 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 = context.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 false;
- }
- long totalFreed = 0;
- try {
- cursor.moveToFirst();
- while (!cursor.isAfterLast() && totalFreed < targetBytes) {
- File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.Impl._DATA)));
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
- file.length() + " bytes");
- }
- totalFreed += file.length();
- file.delete();
- long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
- context.getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
- null, null);
- cursor.moveToNext();
- }
- } finally {
- cursor.close();
- }
- if (Constants.LOGV) {
- if (totalFreed > 0) {
- Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
- targetBytes + " requested");
- }
- }
- return totalFreed > 0;
- }
-
- /**
* Returns whether the network is available
*/
public static boolean isNetworkAvailable(SystemFacade system) {
@@ -864,7 +677,4 @@ public class Helpers {
}
return sb.toString();
}
- static final File getDownloadsDataDirectory(Context context) {
- return context.getCacheDir();
- }
}
diff --git a/src/com/android/providers/downloads/StopRequestException.java b/src/com/android/providers/downloads/StopRequestException.java
new file mode 100644
index 0000000..0ccf53c
--- /dev/null
+++ b/src/com/android/providers/downloads/StopRequestException.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Raised to indicate that the current request should be stopped immediately.
+ *
+ * Note the message passed to this exception will be logged and therefore must be guaranteed
+ * not to contain any PII, meaning it generally can't include any information about the request
+ * URI, headers, or destination filename.
+ */
+class StopRequestException extends Exception {
+ public int mFinalStatus;
+
+ public StopRequestException(int finalStatus, String message) {
+ super(message);
+ mFinalStatus = finalStatus;
+ }
+
+ public StopRequestException(int finalStatus, String message, Throwable throwable) {
+ super(message, throwable);
+ mFinalStatus = finalStatus;
+ }
+}
diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java
new file mode 100644
index 0000000..d7d0a7a
--- /dev/null
+++ b/src/com/android/providers/downloads/StorageManager.java
@@ -0,0 +1,452 @@
+/*
+ * 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 android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.drm.mobile1.DrmRawContent;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.StatFs;
+import android.provider.Downloads;
+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;
+
+/**
+ * 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;
+
+ /** the Singleton instance of this class.
+ * TODO: once DownloadService is refactored into a long-living object, there is no need
+ * for this Singleton'ing.
+ */
+ private static StorageManager sSingleton = null;
+
+ /** 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;
+
+ /**
+ * maintains Singleton instance of this class
+ */
+ synchronized static StorageManager getInstance(Context context) {
+ if (sSingleton == null) {
+ sSingleton = new StorageManager(context);
+ }
+ return sSingleton;
+ }
+
+ private StorageManager(Context context) { // constructor is private
+ mContext = context;
+ mDownloadDataDir = context.getCacheDir();
+ 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;
+ 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(mSystemCacheDir);
+ if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
+ // print a warning
+ Log.w(Constants.TAG, "Downloads data dir: " + root +
+ " is running low on space. space available (in b): " + bytesAvailable);
+ } else if (bytesAvailable < targetBytes) {
+ // Insufficient space; make space.
+ discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold);
+ removeSpuriousFiles();
+ bytesAvailable = getAvailableBytesInDownloadsDataDir(mSystemCacheDir);
+ }
+ }
+ 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:
+ // DRM messages should be temporarily stored internally and then passed to
+ // the DRM content provider
+ if (DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
+ return mDownloadDataDir;
+ }
+ throw new IllegalStateException("unexpected value for destination: " + destination);
+ }
+ }
+
+ File getDownloadDataDirectory() {
+ return mDownloadDataDir;
+ }
+
+ /**
+ * 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 (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 {
+ while (cursor.moveToNext() && totalFreed < targetBytes) {
+ File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.Impl._DATA)));
+ if (Constants.LOGV) {
+ Log.i(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 (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>();
+ files.addAll(Arrays.asList(mSystemCacheDir.listFiles()));
+ files.addAll(Arrays.asList(mDownloadDataDir.listFiles()));
+ 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()) {
+ files.remove(cursor.getString(0));
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ // delete the files not found in the database
+ for (File file : files) {
+ if (file.getName().equals(Constants.KNOWN_SPURIOUS_FILENAME) ||
+ file.getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
+ continue;
+ }
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "deleting spurious file " + file.getAbsolutePath());
+ }
+ file.delete();
+ }
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
index d7f4787..c3ac890 100644
--- a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
@@ -65,7 +65,7 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
Integer.toString(Downloads.Impl.DESTINATION_CACHE_PARTITION));
runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
- assertStartsWith(Helpers.getDownloadsDataDirectory(getContext()).getAbsolutePath(),
+ assertStartsWith(getContext().getCacheDir().getAbsolutePath(),
getDownloadFilename(downloadUri));
}
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index 96fbaab..64c1953 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -25,7 +25,6 @@ import android.net.Uri;
import android.os.Environment;
import android.provider.Downloads;
import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.Suppress;
import tests.http.MockResponse;
import tests.http.RecordedRequest;
@@ -386,10 +385,6 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
runSimpleFailureTest(DownloadManager.ERROR_HTTP_DATA_ERROR);
}
- /**
- * un-suppress this test once the bug 3286430 is fixed
- */
- @Suppress
public void testInsufficientSpace() throws Exception {
// this would be better done by stubbing the system API to check available space, but in the
// meantime, just use an absurdly large header value