diff options
author | Steve Howard <showard@google.com> | 2010-09-20 19:32:27 -0700 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2010-09-20 19:32:27 -0700 |
commit | 99c0a5d260376d26281497e8fc2fd043d431f8c3 (patch) | |
tree | 4622235fc65179505d51b9f7040a79c59254c22d | |
parent | b4b4817f61386935e076dcd6758887fc6c5213e2 (diff) | |
parent | 2031fa7bad5ef3e31cf0ea73d0b9bf4670842cf0 (diff) | |
download | android_packages_providers_DownloadProvider-99c0a5d260376d26281497e8fc2fd043d431f8c3.tar.gz android_packages_providers_DownloadProvider-99c0a5d260376d26281497e8fc2fd043d431f8c3.tar.bz2 android_packages_providers_DownloadProvider-99c0a5d260376d26281497e8fc2fd043d431f8c3.zip |
am 2031fa7b: am dc738781: Merge "Improve file error reporting + new detailed error messages in UI" into gingerbread
Merge commit '2031fa7bad5ef3e31cf0ea73d0b9bf4670842cf0'
* commit '2031fa7bad5ef3e31cf0ea73d0b9bf4670842cf0':
Improve file error reporting + new detailed error messages in UI
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 23584f85..6d397d3d 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 d680e083..a7eb185c 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -434,9 +434,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 a74810fe..0b696493 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(); |