summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteve Howard <showard@google.com>2010-09-16 12:04:17 -0700
committerSteve Howard <showard@google.com>2010-09-20 13:45:30 -0700
commitb9a0ad7182209d4aca708e13e876e9b1b43ffafc (patch)
treee49d39e80dfc9c2ffc6a43c81f08ee0edce26c31
parentdbefa6f5eff88f97dd91a8adfd65dbd946adb99e (diff)
downloadandroid_packages_providers_DownloadProvider-b9a0ad7182209d4aca708e13e876e9b1b43ffafc.tar.gz
android_packages_providers_DownloadProvider-b9a0ad7182209d4aca708e13e876e9b1b43ffafc.tar.bz2
android_packages_providers_DownloadProvider-b9a0ad7182209d4aca708e13e876e9b1b43ffafc.zip
Improve file error reporting + new detailed error messages in UI
* support new error code for "destination file already exists" * improve error handling for various file error cases to return a more specific error code when appropriate * make UI support more detailed error messages for some cases * use Uri.getPath() instead of Uri.getSchemeSpecificPart() for file URIs Change-Id: Icb01d4d3b47c7776be3ddcd8347212e950cd023e
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java8
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java15
-rw-r--r--src/com/android/providers/downloads/Helpers.java147
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java6
-rw-r--r--ui/res/values/strings.xml19
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadList.java63
6 files changed, 184 insertions, 74 deletions
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index df7ca71d..102c611d 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -121,6 +121,7 @@ public final class DownloadProvider extends ContentProvider {
Downloads.Impl.COLUMN_DESCRIPTION,
Downloads.Impl.COLUMN_URI,
Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
+ Downloads.Impl.COLUMN_FILE_NAME_HINT,
};
private static HashSet<String> sAppReadableColumnsSet;
@@ -508,9 +509,12 @@ public final class DownloadProvider extends ContentProvider {
if (!uri.getScheme().equals("file")) {
throw new IllegalArgumentException("Not a file URI: " + uri);
}
- File path = new File(uri.getSchemeSpecificPart());
+ String path = uri.getPath();
+ if (path == null) {
+ throw new IllegalArgumentException("Invalid file URI: " + uri);
+ }
String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
- if (!path.getPath().startsWith(externalPath)) {
+ if (!path.startsWith(externalPath)) {
throw new SecurityException("Destination must be on external storage: " + uri);
}
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index b2353e16..57007f49 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -426,9 +426,18 @@ public class DownloadThread extends Thread {
}
return;
} catch (IOException ex) {
- if (mInfo.isOnCache()
- && Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
- continue;
+ if (mInfo.isOnCache()) {
+ if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
+ continue;
+ }
+ } else if (!Helpers.isExternalMediaMounted()) {
+ throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
+ }
+
+ long availableBytes =
+ Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
+ if (availableBytes < bytesRead) {
+ throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, ex);
}
throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR, ex);
}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index f8900d9a..f1f23b17 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -104,7 +104,7 @@ public class Helpers {
String fullFilename;
try {
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
- fullFilename = getPathForFileUri(hint);
+ fullFilename = getPathForFileUri(hint, contentLength);
} else {
fullFilename = chooseFullPath(context, url, hint, contentDisposition,
contentLocation, mimeType, destination,
@@ -117,16 +117,38 @@ public class Helpers {
return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
}
- private static String getPathForFileUri(String hint) throws GenerateSaveFileError {
- String path = Uri.parse(hint).getSchemeSpecificPart();
+ private static String getPathForFileUri(String hint, long contentLength)
+ throws GenerateSaveFileError {
+ if (!isExternalMediaMounted()) {
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
+ }
+ String path = Uri.parse(hint).getPath();
if (new File(path).exists()) {
Log.d(Constants.TAG, "File already exists: " + path);
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR);
+ }
+ if (getAvailableBytes(getFilesystemRoot(path)) < contentLength) {
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
}
return path;
}
+ /**
+ * @return the root of the filesystem containing the given path
+ */
+ public static File getFilesystemRoot(String path) {
+ File cache = Environment.getDownloadCacheDirectory();
+ if (path.startsWith(cache.getPath())) {
+ return cache;
+ }
+ 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)
@@ -203,77 +225,78 @@ public class Helpers {
private static File locateDestinationDirectory(Context context, String mimeType,
int destination, long contentLength)
throws GenerateSaveFileError {
- File base = null;
- StatFs stat = null;
// 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
|| DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
- // Saving to internal storage.
- base = Environment.getDownloadCacheDirectory();
- stat = new StatFs(base.getPath());
-
- /*
- * Check whether there's enough space on the target filesystem to save the file.
- * Put a bit of margin (in case creating the file grows the system by a few blocks).
- */
- int blockSize = stat.getBlockSize();
- long bytesAvailable = blockSize * ((long) stat.getAvailableBlocks() - 4);
- while (bytesAvailable < contentLength) {
- // Insufficient space; try discarding purgeable files.
- if (!discardPurgeableFiles(context, contentLength - bytesAvailable)) {
- // No files to purge, give up.
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "download aborted - not enough free space in internal storage");
- }
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
- } else {
- // Recalculate available space and try again.
- stat.restat(base.getPath());
- bytesAvailable = blockSize * ((long) stat.getAvailableBlocks() - 4);
- }
- }
- } else if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- // Saving to external storage (SD card).
- String root = Environment.getExternalStorageDirectory().getPath();
- stat = new StatFs(root);
-
- /*
- * Check whether there's enough space on the target filesystem to save the file.
- * Put a bit of margin (in case creating the file grows the system by a few blocks).
- */
- if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) {
- // Insufficient space.
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - not enough free space");
- }
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
- }
+ return getCacheDestination(context, contentLength);
+ }
- base = new File(root + 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.
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - can't create base directory "
- + base.getPath());
- }
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
- }
- } else {
- // No SD card found.
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - no external storage");
- }
+ return getExternalDestination(contentLength);
+ }
+
+ private static File getExternalDestination(long contentLength) throws GenerateSaveFileError {
+ if (!isExternalMediaMounted()) {
throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
}
+ 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);
+ }
+
+ 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.
+ Log.d(Constants.TAG, "download aborted - can't create base directory "
+ + base.getPath());
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
+ }
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)
+ throws GenerateSaveFileError {
+ File base;
+ base = Environment.getDownloadCacheDirectory();
+ long bytesAvailable = getAvailableBytes(base);
+ while (bytesAvailable < contentLength) {
+ // Insufficient space; try discarding purgeable files.
+ if (!discardPurgeableFiles(context, contentLength - bytesAvailable)) {
+ // No files to purge, give up.
+ Log.d(Constants.TAG,
+ "download aborted - not enough free space in internal storage");
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
+ }
+ 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;
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index d577e2c8..554cc1ea 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -292,7 +292,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
Uri localUri = Uri.parse(download.getStringField(DownloadManager.COLUMN_LOCAL_URI));
assertEquals(destination, localUri);
- InputStream stream = new FileInputStream(destination.getSchemeSpecificPart());
+ InputStream stream = new FileInputStream(destination.getPath());
try {
assertEquals(FILE_CONTENT, readStream(stream));
} finally {
@@ -525,12 +525,12 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
public void testExistingFile() throws Exception {
Uri destination = getExternalUri();
- new File(destination.getSchemeSpecificPart()).createNewFile();
+ new File(destination.getPath()).createNewFile();
enqueueEmptyResponse(HTTP_OK);
Download download = enqueueRequest(getRequest().setDestinationUri(destination));
download.runUntilStatus(DownloadManager.STATUS_FAILED);
- assertEquals(DownloadManager.ERROR_FILE_ERROR,
+ assertEquals(DownloadManager.ERROR_FILE_ALREADY_EXISTS,
download.getLongField(DownloadManager.COLUMN_ERROR_CODE));
}
diff --git a/ui/res/values/strings.xml b/ui/res/values/strings.xml
index 063ed04e..bb3654af 100644
--- a/ui/res/values/strings.xml
+++ b/ui/res/values/strings.xml
@@ -65,6 +65,25 @@
<!-- Text for dialog when user clicks on a completed download but the file is missing
[CHAR LIMIT=200] -->
<string name="dialog_file_missing_body">The downloaded file cannot be found.</string>
+ <!-- Text for dialog when user clicks on a download that failed due to insufficient space on
+ external storage [CHAR LIMIT=200] -->
+ <string name="dialog_insufficient_space_on_external">Cannot finish download. There is not
+ enough space on external storage.</string>
+ <!-- Text for dialog when user clicks on a download that failed due to insufficient space on
+ the internal download cache [CHAR LIMIT=200] -->
+ <string name="dialog_insufficient_space_on_cache">Cannot finish download. There is not enough
+ space on internal download storage.</string>
+ <!-- Text for dialog when user clicks on a download that failed because it was interrupted and
+ the server doesn't support resuming downloads [CHAR LIMIT=200] -->
+ <string name="dialog_cannot_resume">Download interrupted. It cannot be resumed.</string>
+ <!-- Text for dialog when user clicks on a download that failed because the requested
+ destination file already exists [CHAR LIMIT=200] -->
+ <string name="dialog_file_already_exists">Cannot download. The destination file already exists.
+ </string>
+ <!-- Text for dialog when user clicks on a download that failed because it was requested to go
+ on the external media, which is not mounted [CHAR LIMIT=200] -->
+ <string name="dialog_media_not_found">Cannot download. The external media is not available.
+ </string>
<!-- Text for a toast appearing when a user clicks on a completed download, informing the user
that there is no application on the device that can open the file that was downloaded
[CHAR LIMIT=200] -->
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadList.java b/ui/src/com/android/providers/downloads/ui/DownloadList.java
index 71e17233..0cbffeb2 100644
--- a/ui/src/com/android/providers/downloads/ui/DownloadList.java
+++ b/ui/src/com/android/providers/downloads/ui/DownloadList.java
@@ -29,6 +29,7 @@ import android.database.Cursor;
import android.net.DownloadManager;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Environment;
import android.os.Handler;
import android.provider.Downloads;
import android.util.Log;
@@ -80,6 +81,7 @@ public class DownloadList extends Activity
private int mIdColumnId;
private int mLocalUriColumnId;
private int mMediaTypeColumnId;
+ private int mErrorCodeColumndId;
private boolean mIsSortedBySize = false;
private Set<Long> mSelectedIds = new HashSet<Long>();
@@ -130,6 +132,8 @@ public class DownloadList extends Activity
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
mMediaTypeColumnId =
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE);
+ mErrorCodeColumndId =
+ mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ERROR_CODE);
mDateSortedAdapter = new DateSortedDownloadAdapter(this, mDateSortedCursor, this);
mDateOrderedListView.setAdapter(mDateSortedAdapter);
@@ -305,7 +309,8 @@ public class DownloadList extends Activity
getContentResolver().openFileDescriptor(localUri, "r").close();
} catch (FileNotFoundException exc) {
Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
- showFailedDialog(cursor.getLong(mIdColumnId), R.string.dialog_file_missing_body);
+ showFailedDialog(cursor.getLong(mIdColumnId),
+ getString(R.string.dialog_file_missing_body));
return;
} catch (IOException exc) {
// close() failed, not a problem
@@ -345,15 +350,65 @@ public class DownloadList extends Activity
break;
case DownloadManager.STATUS_FAILED:
- showFailedDialog(id, R.string.dialog_failed_body);
+ showFailedDialog(id, getErrorMessage(cursor));
break;
}
}
- private void showFailedDialog(long downloadId, int dialogBodyResource) {
+ /**
+ * @return the appropriate error message for the failed download pointed to by cursor
+ */
+ private String getErrorMessage(Cursor cursor) {
+ switch (cursor.getInt(mErrorCodeColumndId)) {
+ case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
+ if (isOnExternalStorage(cursor)) {
+ return getString(R.string.dialog_file_already_exists);
+ } else {
+ // the download manager should always find a free filename for cache downloads,
+ // so this indicates a strange internal error
+ return getUnknownErrorMessage();
+ }
+
+ case DownloadManager.ERROR_INSUFFICIENT_SPACE:
+ if (isOnExternalStorage(cursor)) {
+ return getString(R.string.dialog_insufficient_space_on_external);
+ } else {
+ return getString(R.string.dialog_insufficient_space_on_cache);
+ }
+
+ case DownloadManager.ERROR_DEVICE_NOT_FOUND:
+ return getString(R.string.dialog_media_not_found);
+
+ case DownloadManager.ERROR_CANNOT_RESUME:
+ return getString(R.string.dialog_cannot_resume);
+
+ default:
+ return getUnknownErrorMessage();
+ }
+ }
+
+ private boolean isOnExternalStorage(Cursor cursor) {
+ String localUriString = cursor.getString(mLocalUriColumnId);
+ if (localUriString == null) {
+ return false;
+ }
+ Uri localUri = Uri.parse(localUriString);
+ if (!localUri.getScheme().equals("file")) {
+ return false;
+ }
+ String path = localUri.getPath();
+ String externalRoot = Environment.getExternalStorageDirectory().getPath();
+ return path.startsWith(externalRoot);
+ }
+
+ private String getUnknownErrorMessage() {
+ return getString(R.string.dialog_failed_body);
+ }
+
+ private void showFailedDialog(long downloadId, String dialogBody) {
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_not_available)
- .setMessage(getResources().getString(dialogBodyResource))
+ .setMessage(dialogBody)
.setPositiveButton(R.string.remove_download, getDeleteClickHandler(downloadId))
.setNegativeButton(R.string.retry_download, getRestartClickHandler(downloadId))
.show();