am cc167f03: (-s ours) am bb611587: (-s ours) Import translations. DO NOT MERGE
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / Helpers.java
index 543b652..eb07139 100644 (file)
 
 package com.android.providers.downloads;
 
+import static com.android.providers.downloads.Constants.TAG;
+
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.FileUtils;
 import android.os.SystemClock;
 import android.provider.Downloads;
 import android.util.Log;
 import android.webkit.MimeTypeMap;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.Random;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -43,6 +44,8 @@ public class Helpers {
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
             Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
 
+    private static final Object sUniqueLock = new Object();
+
     private Helpers() {
     }
 
@@ -66,105 +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,
-            boolean isPublicApi, StorageManager storageManager) throws StopRequestException {
-        checkCanHandleDownload(context, mimeType, destination, isPublicApi);
-        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);
         }
-        storageManager.verifySpace(destination, path, contentLength);
-        path = getFullPath(path, mimeType, destination, base);
-        if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
-            path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
-        }
-        return path;
-    }
 
-    static String getFullPath(String filename, String mimeType, int destination,
-        File base) throws StopRequestException {
-        // Split filename between base and extension
-        // Add an extension if filename does not have one
-        String extension = null;
-        int dotIndex = filename.lastIndexOf('.');
-        boolean missingExtension = dotIndex < 0 || dotIndex < filename.lastIndexOf('/');
-        if (missingExtension) {
-            extension = chooseExtensionFromMimeType(mimeType, true);
-        } else {
-            extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
-            filename = filename.substring(0, dotIndex);
+        // Ensure target directories are ready
+        for (File test : parentTest) {
+            if (!(test.isDirectory() || test.mkdirs())) {
+                throw new IOException("Failed to create parent for " + test);
+            }
         }
 
-        boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
-
-        if (base != null) {
-            filename = base.getPath() + File.separator + filename;
+        if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
+            name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name);
         }
 
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "target file: " + filename + extension);
+        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) {
+                prefix = name;
+                suffix = "";
+            } else {
+                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) {
+                prefix = name;
+                suffix = chooseExtensionFromMimeType(mimeType, true);
+            } else {
+                prefix = name.substring(0, dotIndex);
+                suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex);
+            }
         }
-        return chooseUniqueFilename(destination, filename, extension, recoveryDir);
-    }
 
-    private static void checkCanHandleDownload(Context context, String mimeType, int destination,
-            boolean isPublicApi) throws StopRequestException {
-        if (isPublicApi) {
-            return;
-        }
+        synchronized (sUniqueLock) {
+            name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
 
-        if (destination == Downloads.Impl.DESTINATION_EXTERNAL
-                || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
-            if (mimeType == null) {
-                throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
-                        "external download with no mime type not allowed");
-            }
-            if (!DownloadDrmHelper.isDrmMimeType(context, mimeType)) {
-                // Check to see if we are allowed to download this file. Only files
-                // that can be handled by the platform can be downloaded.
-                // special case DRM files, which we should always allow downloading.
-                Intent intent = new Intent(Intent.ACTION_VIEW);
-
-                // We can provide data as either content: or file: URIs,
-                // so allow both.  (I think it would be nice if we just did
-                // everything as content: URIs)
-                // Actually, right now the download manager's UId restrictions
-                // prevent use from using content: so it's got to be file: or
-                // nothing
-
-                PackageManager pm = context.getPackageManager();
-                intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
-                ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-                //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
-
-                if (ri == null) {
-                    if (Constants.LOGV) {
-                        Log.v(Constants.TAG, "no handler found for type " + mimeType);
-                    }
-                    throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
-                            "no handler found for this download type");
-                }
-            }
+            // Claim this filename inside lock to prevent other threads from
+            // clobbering us. We're not paranoid enough to use O_EXCL.
+            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
@@ -310,18 +287,25 @@ public class Helpers {
         return extension;
     }
 
-    private static String chooseUniqueFilename(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.
@@ -339,35 +323,86 @@ 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");
     }
 
     /**
-     * Returns whether the network is available
+     * Checks whether the filename looks legitimate for security purposes. This
+     * prevents us from opening files that aren't actually downloads.
      */
-    public static boolean isNetworkAvailable(SystemFacade system) {
-        return system.getActiveNetworkType() != null;
+    static boolean isFilenameValid(Context context, File file) {
+        final File[] whitelist;
+        try {
+            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 (File testDir : whitelist) {
+            if (FileUtils.contains(testDir, file)) {
+                return true;
+            }
+        }
+
+        return false;
     }
 
-    /**
-     * Checks whether the filename looks legitimate
-     */
-    static boolean isFilenameValid(String filename, File downloadsDataDir) {
-        filename = filename.replaceFirst("/+", "/"); // normalize leading slashes
-        return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
-                || filename.startsWith(downloadsDataDir.toString())
-                || filename.startsWith(Environment.getExternalStorageDirectory().toString());
+    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);
+        }
     }
 
     /**