summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/Helpers.java
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2014-01-30 15:01:39 -0800
committerJeff Sharkey <jsharkey@android.com>2014-02-06 10:42:46 -0800
commitdffbb9c4567e9d29d19964a83129e38dceab7055 (patch)
tree773bb59bc04f75e19e3a39acba06de574f75a385 /src/com/android/providers/downloads/Helpers.java
parent9b731a5521f569c91aeb419d43fa098a34cf78cb (diff)
downloadandroid_packages_providers_DownloadProvider-dffbb9c4567e9d29d19964a83129e38dceab7055.tar.gz
android_packages_providers_DownloadProvider-dffbb9c4567e9d29d19964a83129e38dceab7055.tar.bz2
android_packages_providers_DownloadProvider-dffbb9c4567e9d29d19964a83129e38dceab7055.zip
Many improvements to download storage management.
Change all data transfer to occur through FileDescriptors instead of relying on local files. This paves the way for downloading directly to content:// Uris in the future. Rewrite storage management logic to preflight download when size is known. If enough space is found, immediately reserve the space with fallocate(), advising the kernel block allocator to try giving us a contiguous block regions to reduce fragmentation. When preflighting on internal storage or emulated external storage, ask PackageManager to clear private app caches to free up space. Since we fallocate() the entire file, use the database as the source of truth for resume locations, which requires that we fsync() before each database update. Store in-progress downloads in separate directories to keep the OS from deleting out from under us. Clean up filename generation logic to break ties in this new dual-directory case. Clearer enforcement of successful download preconditions around content lengths and ETags. Move all database field mutations to clearer DownloadInfoDelta object, and write back through single code path. Catch and log uncaught exceptions from DownloadThread. Tests to verify new storage behaviors. Fixed existing test to reflect correct RFC behavior. Bug: 5287571, 3213677, 12663412 Change-Id: I6bb905eca7c7d1a6bc88df3db28b65d70f660221
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r--src/com/android/providers/downloads/Helpers.java219
1 files changed, 125 insertions, 94 deletions
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 3562dac7..eb071395 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -21,6 +21,7 @@ import static com.android.providers.downloads.Constants.TAG;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
+import android.os.FileUtils;
import android.os.SystemClock;
import android.provider.Downloads;
import android.util.Log;
@@ -68,98 +69,79 @@ public class Helpers {
/**
* Creates a filename (where the file should be saved) from info about a download.
+ * This file will be touched to reserve it.
*/
- static String generateSaveFile(
- Context context,
- String url,
- String hint,
- String contentDisposition,
- String contentLocation,
- String mimeType,
- int destination,
- long contentLength,
- StorageManager storageManager) throws StopRequestException {
- if (contentLength < 0) {
- contentLength = 0;
- }
- String path;
- File base = null;
+ static String generateSaveFile(Context context, String url, String hint,
+ String contentDisposition, String contentLocation, String mimeType, int destination)
+ throws IOException {
+
+ final File parent;
+ final File[] parentTest;
+ String name = null;
+
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
- path = Uri.parse(hint).getPath();
+ final File file = new File(Uri.parse(hint).getPath());
+ parent = file.getParentFile().getAbsoluteFile();
+ parentTest = new File[] { parent };
+ name = file.getName();
} else {
- base = storageManager.locateDestinationDirectory(mimeType, destination,
- contentLength);
- path = chooseFilename(url, hint, contentDisposition, contentLocation,
- destination);
+ parent = getRunningDestinationDirectory(context, destination);
+ parentTest = new File[] {
+ parent,
+ getSuccessDestinationDirectory(context, destination)
+ };
+ name = chooseFilename(url, hint, contentDisposition, contentLocation);
+ }
+
+ // Ensure target directories are ready
+ for (File test : parentTest) {
+ if (!(test.isDirectory() || test.mkdirs())) {
+ throw new IOException("Failed to create parent for " + test);
+ }
}
- storageManager.verifySpace(destination, path, contentLength);
+
if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
- path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
+ name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name);
}
- path = getFullPath(path, mimeType, destination, base);
- return path;
- }
- static String getFullPath(String filename, String mimeType, int destination, File base)
- throws StopRequestException {
- String extension = null;
- int dotIndex = filename.lastIndexOf('.');
- boolean missingExtension = dotIndex < 0 || dotIndex < filename.lastIndexOf('/');
+ final String prefix;
+ final String suffix;
+ final int dotIndex = name.lastIndexOf('.');
+ final boolean missingExtension = dotIndex < 0;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
// Destination is explicitly set - do not change the extension
if (missingExtension) {
- extension = "";
+ prefix = name;
+ suffix = "";
} else {
- extension = filename.substring(dotIndex);
- filename = filename.substring(0, dotIndex);
+ prefix = name.substring(0, dotIndex);
+ suffix = name.substring(dotIndex);
}
} else {
// Split filename between base and extension
// Add an extension if filename does not have one
if (missingExtension) {
- extension = chooseExtensionFromMimeType(mimeType, true);
+ prefix = name;
+ suffix = chooseExtensionFromMimeType(mimeType, true);
} else {
- extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
- filename = filename.substring(0, dotIndex);
+ prefix = name.substring(0, dotIndex);
+ suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex);
}
}
- boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
-
- if (base != null) {
- filename = base.getPath() + File.separator + filename;
- }
-
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "target file: " + filename + extension);
- }
-
synchronized (sUniqueLock) {
- final String path = chooseUniqueFilenameLocked(
- destination, filename, extension, recoveryDir);
+ name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
// Claim this filename inside lock to prevent other threads from
// clobbering us. We're not paranoid enough to use O_EXCL.
- try {
- File file = new File(path);
- File parent = file.getParentFile();
-
- // Make sure the parent directories exists before generates new file
- if (parent != null && !parent.exists()) {
- parent.mkdirs();
- }
-
- file.createNewFile();
- } catch (IOException e) {
- throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
- "Failed to create target file " + path, e);
- }
- return path;
+ final File file = new File(parent, name);
+ file.createNewFile();
+ return file.getAbsolutePath();
}
}
private static String chooseFilename(String url, String hint, String contentDisposition,
- String contentLocation, int destination) {
+ String contentLocation) {
String filename = null;
// First, try to use the hint from the application, if there's one
@@ -305,18 +287,25 @@ public class Helpers {
return extension;
}
- private static String chooseUniqueFilenameLocked(int destination, String filename,
- String extension, boolean recoveryDir) throws StopRequestException {
- String fullFilename = filename + extension;
- if (!new File(fullFilename).exists()
- && (!recoveryDir ||
- (destination != Downloads.Impl.DESTINATION_CACHE_PARTITION &&
- destination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION &&
- destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE &&
- destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING))) {
- return fullFilename;
- }
- filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
+ private static boolean isFilenameAvailableLocked(File[] parents, String name) {
+ if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false;
+
+ for (File parent : parents) {
+ if (new File(parent, name).exists()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static String generateAvailableFilenameLocked(
+ File[] parents, String prefix, String suffix) throws IOException {
+ String name = prefix + suffix;
+ if (isFilenameAvailableLocked(parents, name)) {
+ return name;
+ }
+
/*
* This number is used to generate partially randomized filenames to avoid
* collisions.
@@ -334,39 +323,38 @@ public class Helpers {
int sequence = 1;
for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
for (int iteration = 0; iteration < 9; ++iteration) {
- fullFilename = filename + sequence + extension;
- if (!new File(fullFilename).exists()) {
- return fullFilename;
- }
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
+ name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix;
+ if (isFilenameAvailableLocked(parents, name)) {
+ return name;
}
sequence += sRandom.nextInt(magnitude) + 1;
}
}
- throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
- "failed to generate an unused filename on internal download storage");
+
+ throw new IOException("Failed to generate an available filename");
}
/**
- * Checks whether the filename looks legitimate
+ * Checks whether the filename looks legitimate for security purposes. This
+ * prevents us from opening files that aren't actually downloads.
*/
- static boolean isFilenameValid(String filename, File downloadsDataDir) {
- final String[] whitelist;
+ static boolean isFilenameValid(Context context, File file) {
+ final File[] whitelist;
try {
- filename = new File(filename).getCanonicalPath();
- whitelist = new String[] {
- downloadsDataDir.getCanonicalPath(),
- Environment.getDownloadCacheDirectory().getCanonicalPath(),
- Environment.getExternalStorageDirectory().getCanonicalPath(),
+ file = file.getCanonicalFile();
+ whitelist = new File[] {
+ context.getFilesDir().getCanonicalFile(),
+ context.getCacheDir().getCanonicalFile(),
+ Environment.getDownloadCacheDirectory().getCanonicalFile(),
+ Environment.getExternalStorageDirectory().getCanonicalFile(),
};
} catch (IOException e) {
Log.w(TAG, "Failed to resolve canonical path: " + e);
return false;
}
- for (String test : whitelist) {
- if (filename.startsWith(test)) {
+ for (File testDir : whitelist) {
+ if (FileUtils.contains(testDir, file)) {
return true;
}
}
@@ -374,6 +362,49 @@ public class Helpers {
return false;
}
+ public static File getRunningDestinationDirectory(Context context, int destination)
+ throws IOException {
+ return getDestinationDirectory(context, destination, true);
+ }
+
+ public static File getSuccessDestinationDirectory(Context context, int destination)
+ throws IOException {
+ return getDestinationDirectory(context, destination, false);
+ }
+
+ private static File getDestinationDirectory(Context context, int destination, boolean running)
+ throws IOException {
+ switch (destination) {
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION:
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
+ if (running) {
+ return context.getFilesDir();
+ } else {
+ return context.getCacheDir();
+ }
+
+ case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
+ if (running) {
+ return new File(Environment.getDownloadCacheDirectory(),
+ Constants.DIRECTORY_CACHE_RUNNING);
+ } else {
+ return Environment.getDownloadCacheDirectory();
+ }
+
+ case Downloads.Impl.DESTINATION_EXTERNAL:
+ final File target = new File(
+ Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
+ if (!target.isDirectory() && target.mkdirs()) {
+ throw new IOException("unable to create external downloads directory");
+ }
+ return target;
+
+ default:
+ throw new IllegalStateException("unexpected destination: " + destination);
+ }
+ }
+
/**
* Checks whether this looks like a legitimate selection parameter
*/