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 42a49f1..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.FileNotFoundException;
-import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.Random;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -44,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() {
     }
 
@@ -73,209 +68,80 @@ public class Helpers {
     }
 
     /**
-     * Exception thrown from methods called by generateSaveFile() for any fatal error.
-     */
-    private static class GenerateSaveFileError extends Exception {
-        int mStatus;
-
-        public GenerateSaveFileError(int status) {
-            mStatus = status;
-        }
-    }
-
-    /**
-     * Creates a filename (where the file should be saved) from a uri.
+     * Creates a filename (where the file should be saved) from info about a download.
+     * This file will be touched to reserve it.
      */
-    public static DownloadFileInfo generateSaveFile(
-            Context context,
-            String url,
-            String hint,
-            String contentDisposition,
-            String contentLocation,
-            String mimeType,
-            int destination,
-            long contentLength,
-            boolean isPublicApi) throws FileNotFoundException {
-
-        if (!canHandleDownload(context, mimeType, destination, isPublicApi)) {
-            return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
-        }
-
-        String fullFilename;
-        try {
-            if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
-                fullFilename = getPathForFileUri(hint);
-            } else {
-                fullFilename = chooseFullPath(context, url, hint, contentDisposition,
-                                              contentLocation, mimeType, destination,
-                                              contentLength);
-            }
-        } catch (GenerateSaveFileError exc) {
-            return new DownloadFileInfo(null, null, exc.mStatus);
-        }
-
-        return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
-    }
-
-    private static String getPathForFileUri(String hint) throws GenerateSaveFileError {
-        String path = Uri.parse(hint).getSchemeSpecificPart();
-        if (new File(path).exists()) {
-            Log.d(Constants.TAG, "File already exists: " + path);
-            throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
-        }
-
-        return 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);
-
-        // Split filename between base and extension
-        // Add an extension if filename does not have one
-        String extension = null;
-        int dotIndex = filename.indexOf('.');
-        if (dotIndex < 0) {
-            extension = chooseExtensionFromMimeType(mimeType, true);
+    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) {
+            final File file = new File(Uri.parse(hint).getPath());
+            parent = file.getParentFile().getAbsoluteFile();
+            parentTest = new File[] { parent };
+            name = file.getName();
         } else {
-            extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
-            filename = filename.substring(0, dotIndex);
+            parent = getRunningDestinationDirectory(context, destination);
+            parentTest = new File[] {
+                    parent,
+                    getSuccessDestinationDirectory(context, destination)
+            };
+            name = chooseFilename(url, hint, contentDisposition, contentLocation);
         }
 
-        boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
-
-        filename = base.getPath() + File.separator + filename;
-
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "target file: " + filename + extension);
+        // Ensure target directories are ready
+        for (File test : parentTest) {
+            if (!(test.isDirectory() || test.mkdirs())) {
+                throw new IOException("Failed to create parent for " + test);
+            }
         }
 
-        return chooseUniqueFilename(destination, filename, extension, recoveryDir);
-    }
-
-    private static boolean canHandleDownload(Context context, String mimeType, int destination,
-            boolean isPublicApi) {
-        if (isPublicApi) {
-            return true;
+        if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
+            name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name);
         }
 
-        if (destination == Downloads.Impl.DESTINATION_EXTERNAL
-                || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
-            if (mimeType == null) {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "external download with no mime type not allowed");
-                }
-                return false;
+        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);
             }
-            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 (Config.LOGD) {
-                        Log.d(Constants.TAG, "no handler found for type " + mimeType);
-                    }
-                    return false;
-                }
+        } 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 true;
-    }
 
-    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);
-            }
+        synchronized (sUniqueLock) {
+            name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
 
-            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");
-            }
-            throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
+            // 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 base;
     }
 
     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
@@ -346,8 +212,9 @@ public class Helpers {
             filename = Constants.DEFAULT_DL_FILENAME;
         }
 
-        filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
-
+        // The VFAT file system is assumed as target for downloads.
+        // Replace invalid characters according to the specifications of VFAT.
+        filename = replaceInvalidVfatCharacters(filename);
 
         return filename;
     }
@@ -391,12 +258,11 @@ public class Helpers {
     }
 
     private static String chooseExtensionFromFilename(String mimeType, int destination,
-            String filename, int dotIndex) {
+            String filename, int lastDotIndex) {
         String extension = null;
         if (mimeType != null) {
             // Compare the last segment of the extension against the mime type.
             // If there's a mismatch, discard the entire extension.
-            int lastDotIndex = filename.lastIndexOf('.');
             String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
                     filename.substring(lastDotIndex + 1));
             if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
@@ -416,22 +282,30 @@ public class Helpers {
             if (Constants.LOGVV) {
                 Log.v(Constants.TAG, "keeping extension");
             }
-            extension = filename.substring(dotIndex);
+            extension = filename.substring(lastDotIndex);
         }
         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_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.
@@ -449,79 +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);
+
+        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.
      */
-    public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
-        Cursor cursor = context.getContentResolver().query(
-                Downloads.Impl.CONTENT_URI,
-                null,
-                "( " +
-                Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
-                Downloads.Impl.COLUMN_DESTINATION +
-                        " = '" + Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE + "' )",
-                null,
-                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.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
-     */
-    public static boolean isFilenameValid(String filename) {
-        filename = filename.replaceFirst("/+", "/"); // normalize leading slashes
-        return filename.startsWith(Environment.getDownloadCacheDirectory().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);
+        }
     }
 
     /**
@@ -540,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;
@@ -784,4 +665,53 @@ public class Helpers {
                     (c >= '0' && c <= '9');
         }
     }
+
+    /**
+     * Replace invalid filename characters according to
+     * specifications of the VFAT.
+     * @note Package-private due to testing.
+     */
+    private static String replaceInvalidVfatCharacters(String filename) {
+        final char START_CTRLCODE = 0x00;
+        final char END_CTRLCODE = 0x1f;
+        final char QUOTEDBL = 0x22;
+        final char ASTERISK = 0x2A;
+        final char SLASH = 0x2F;
+        final char COLON = 0x3A;
+        final char LESS = 0x3C;
+        final char GREATER = 0x3E;
+        final char QUESTION = 0x3F;
+        final char BACKSLASH = 0x5C;
+        final char BAR = 0x7C;
+        final char DEL = 0x7F;
+        final char UNDERSCORE = 0x5F;
+
+        StringBuffer sb = new StringBuffer();
+        char ch;
+        boolean isRepetition = false;
+        for (int i = 0; i < filename.length(); i++) {
+            ch = filename.charAt(i);
+            if ((START_CTRLCODE <= ch &&
+                ch <= END_CTRLCODE) ||
+                ch == QUOTEDBL ||
+                ch == ASTERISK ||
+                ch == SLASH ||
+                ch == COLON ||
+                ch == LESS ||
+                ch == GREATER ||
+                ch == QUESTION ||
+                ch == BACKSLASH ||
+                ch == BAR ||
+                ch == DEL){
+                if (!isRepetition) {
+                    sb.append(UNDERSCORE);
+                    isRepetition = true;
+                }
+            } else {
+                sb.append(ch);
+                isRepetition = false;
+            }
+        }
+        return sb.toString();
+    }
 }