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 f392f3e..eb07139 100644 (file)
 
 package com.android.providers.downloads;
 
-import android.content.ContentUris;
+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.database.Cursor;
-import android.drm.mobile1.DrmRawContent;
 import android.net.Uri;
 import android.os.Environment;
-import android.os.StatFs;
+import android.os.FileUtils;
 import android.os.SystemClock;
 import android.provider.Downloads;
-import android.util.Config;
 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;
@@ -42,13 +38,14 @@ 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 */
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
             Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
 
+    private static final Object sUniqueLock = new Object();
+
     private Helpers() {
     }
 
@@ -71,234 +68,80 @@ public class Helpers {
     }
 
     /**
-     * Exception thrown from methods called by generateSaveFile() for any fatal error.
+     * Creates a filename (where the file should be saved) from info about a download.
+     * This file will be touched to reserve it.
      */
-    public static class GenerateSaveFileError extends Exception {
-        int mStatus;
-        String mMessage;
+    static String generateSaveFile(Context context, String url, String hint,
+            String contentDisposition, String contentLocation, String mimeType, int destination)
+            throws IOException {
 
-        public GenerateSaveFileError(int status, String message) {
-            mStatus = status;
-            mMessage = message;
-        }
-    }
+        final File parent;
+        final File[] parentTest;
+        String name = null;
 
-    /**
-     * Creates a filename (where the file should be saved) from info about a download.
-     */
-    public static String generateSaveFile(
-            Context context,
-            String url,
-            String hint,
-            String contentDisposition,
-            String contentLocation,
-            String mimeType,
-            int destination,
-            long contentLength,
-            boolean isPublicApi) throws GenerateSaveFileError {
-        checkCanHandleDownload(context, mimeType, destination, isPublicApi);
         if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
-            String path = verifyFileUri(context, hint, contentLength);
-            String c = getFullPath(path, mimeType, destination, null);
-            return c;
+            final File file = new File(Uri.parse(hint).getPath());
+            parent = file.getParentFile().getAbsoluteFile();
+            parentTest = new File[] { parent };
+            name = file.getName();
         } else {
-            return chooseFullPath(context, url, hint, contentDisposition, contentLocation, mimeType,
-                    destination, contentLength);
+            parent = getRunningDestinationDirectory(context, destination);
+            parentTest = new File[] {
+                    parent,
+                    getSuccessDestinationDirectory(context, destination)
+            };
+            name = chooseFilename(url, hint, contentDisposition, contentLocation);
         }
-    }
 
-    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");
+        // Ensure target directories are ready
+        for (File test : parentTest) {
+            if (!(test.isDirectory() || test.mkdirs())) {
+                throw new IOException("Failed to create parent for " + test);
+            }
         }
 
-        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;
+        if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
+            name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name);
         }
-        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);
-    }
 
-    private static String getFullPath(String filename, String mimeType, int destination,
-        File base) throws GenerateSaveFileError {
-        // 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);
+        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 {
-            extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
-            filename = filename.substring(0, 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);
-        }
-        return chooseUniqueFilename(destination, filename, extension, recoveryDir);
-    }
-
-    private static void checkCanHandleDownload(Context context, String mimeType, int destination,
-            boolean isPublicApi) throws GenerateSaveFileError {
-        if (isPublicApi) {
-            return;
-        }
-
-        if (destination == Downloads.Impl.DESTINATION_EXTERNAL
-                || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
-            if (mimeType == null) {
-                throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
-                        "external download with no mime type not allowed");
-            }
-            if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(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 GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
-                            "no handler found for this download type");
-                }
+            // 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);
             }
         }
-    }
-
-    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;
-    }
+        synchronized (sUniqueLock) {
+            name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
 
-    public static boolean isExternalMediaMounted() {
-        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-            // No SD card found.
-            Log.d(Constants.TAG, "no external storage");
-            return false;
+            // 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();
         }
-        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 contentLocation) {
         String filename = null;
 
         // First, try to use the hint from the application, if there's one
@@ -444,18 +287,25 @@ public class Helpers {
         return extension;
     }
 
-    private static String chooseUniqueFilename(int destination, String filename,
-            String extension, boolean recoveryDir) throws GenerateSaveFileError {
-        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.
@@ -473,86 +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 GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR,
-                "failed to generate an unused filename on internal download storage");
+
+        throw new IOException("Failed to generate an available filename");
     }
 
     /**
-     * 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.
+     * Checks whether the filename looks legitimate for security purposes. This
+     * prevents us from opening files that aren't actually downloads.
      */
-    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;
+    static boolean isFilenameValid(Context context, File file) {
+        final File[] whitelist;
         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();
+            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;
         }
-        if (Constants.LOGV) {
-            if (totalFreed > 0) {
-                Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
-                        targetBytes + " requested");
+
+        for (File testDir : whitelist) {
+            if (FileUtils.contains(testDir, file)) {
+                return true;
             }
         }
-        return totalFreed > 0;
+
+        return false;
     }
 
-    /**
-     * Returns whether the network is available
-     */
-    public static boolean isNetworkAvailable(SystemFacade system) {
-        return system.getActiveNetworkType() != null;
+    public static File getRunningDestinationDirectory(Context context, int destination)
+            throws IOException {
+        return getDestinationDirectory(context, destination, true);
     }
 
-    /**
-     * 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 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);
+        }
     }
 
     /**
@@ -571,7 +421,7 @@ public class Helpers {
         } catch (RuntimeException ex) {
             if (Constants.LOGV) {
                 Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
-            } else if (Config.LOGD) {
+            } else if (false) {
                 Log.d(Constants.TAG, "invalid selection triggered " + ex);
             }
             throw ex;
@@ -864,7 +714,4 @@ public class Helpers {
         }
         return sb.toString();
     }
-    static final File getDownloadsDataDirectory(Context context) {
-        return context.getCacheDir();
-    }
 }