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 d8f262c..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.ConnectivityManager;
-import android.net.NetworkInfo;
 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.telephony.TelephonyManager;
-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.File;
+import java.io.IOException;
 import java.util.Random;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -47,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() {
     }
 
@@ -76,160 +68,80 @@ public class Helpers {
     }
 
     /**
-     * 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,
-            int contentLength) throws FileNotFoundException {
+    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 {
+            parent = getRunningDestinationDirectory(context, destination);
+            parentTest = new File[] {
+                    parent,
+                    getSuccessDestinationDirectory(context, destination)
+            };
+            name = chooseFilename(url, hint, contentDisposition, contentLocation);
+        }
 
-        /*
-         * Don't download files that we won't be able to handle
-         */
-        if (destination == Downloads.DESTINATION_EXTERNAL
-                || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
-            if (mimeType == null) {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "external download with no mime type not allowed");
-                }
-                return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
-            }
-            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 new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
-                }
+        // Ensure target directories are ready
+        for (File test : parentTest) {
+            if (!(test.isDirectory() || test.mkdirs())) {
+                throw new IOException("Failed to create parent for " + test);
             }
         }
-        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);
-        } else {
-            extension = chooseExtensionFromFilename(
-                    mimeType, destination, filename, dotIndex);
-            filename = filename.substring(0, dotIndex);
+        if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
+            name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name);
         }
 
-        /*
-         *  Locate the directory where the file will be saved
-         */
-
-        File base = null;
-        StatFs stat = null;
-        // DRM messages should be temporarily stored internally and then passed to 
-        // the DRM content provider
-        if (destination == Downloads.DESTINATION_CACHE_PARTITION
-                || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
-                || destination == Downloads.DESTINATION_CACHE_PARTITION_NOROAMING
-                || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
-            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();
-            for (;;) {
-                int availableBlocks = stat.getAvailableBlocks();
-                if (blockSize * ((long) availableBlocks - 4) >= contentLength) {
-                    break;
-                }
-                if (!discardPurgeableFiles(context,
-                        contentLength - blockSize * ((long) availableBlocks - 4))) {
-                    if (Config.LOGD) {
-                        Log.d(Constants.TAG,
-                                "download aborted - not enough free space in internal storage");
-                    }
-                    return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
-                }
-                stat.restat(base.getPath());
+        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 {
-            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-                String root = Environment.getExternalStorageDirectory().getPath();
-                base = new File(root + Constants.DEFAULT_DL_SUBDIR);
-                if (!base.isDirectory() && !base.mkdir()) {
-                    if (Config.LOGD) {
-                        Log.d(Constants.TAG, "download aborted - can't create base directory "
-                                + base.getPath());
-                    }
-                    return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
-                }
-                stat = new StatFs(base.getPath());
+            // Split filename between base and extension
+            // Add an extension if filename does not have one
+            if (missingExtension) {
+                prefix = name;
+                suffix = chooseExtensionFromMimeType(mimeType, true);
             } else {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "download aborted - no external storage");
-                }
-                return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
-            }
-
-            /*
-             * 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) {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "download aborted - not enough free space");
-                }
-                return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+                prefix = name.substring(0, dotIndex);
+                suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex);
             }
-
         }
 
-        boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
-
-        filename = base.getPath() + File.separator + filename;
-
-        /*
-         * Generate a unique filename, create the file, return it.
-         */
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "target file: " + filename + extension);
-        }
+        synchronized (sUniqueLock) {
+            name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
 
-        String fullFilename = chooseUniqueFilename(
-                destination, filename, extension, recoveryDir);
-        if (fullFilename != null) {
-            return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
-        } else {
-            return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_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();
         }
     }
 
     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
@@ -300,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;
     }
@@ -345,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)) {
@@ -370,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) {
-        String fullFilename = filename + extension;
-        if (!new File(fullFilename).exists()
-                && (!recoveryDir ||
-                (destination != Downloads.DESTINATION_CACHE_PARTITION &&
-                        destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE &&
-                        destination != Downloads.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.
@@ -403,130 +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;
             }
         }
-        return null;
+
+        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.CONTENT_URI,
-                null,
-                "( " +
-                Downloads.COLUMN_STATUS + " = '" + Downloads.STATUS_SUCCESS + "' AND " +
-                Downloads.COLUMN_DESTINATION +
-                        " = '" + Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE + "' )",
-                null,
-                Downloads.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._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._ID));
-                context.getContentResolver().delete(
-                        ContentUris.withAppendedId(Downloads.CONTENT_URI, id), null, null);
-                cursor.moveToNext();
-            }
-        } finally {
-            cursor.close();
-        }
-        if (Constants.LOGV) {
-            if (totalFreed > 0) {
-                Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
-                        targetBytes + " requested");
-            }
+            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;
         }
-        return totalFreed > 0;
-    }
 
-    /**
-     * Returns whether the network is available
-     */
-    public static boolean isNetworkAvailable(Context context) {
-        ConnectivityManager connectivity =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        if (connectivity == null) {
-            Log.w(Constants.TAG, "couldn't get connectivity manager");
-        } else {
-            NetworkInfo[] info = connectivity.getAllNetworkInfo();
-            if (info != null) {
-                for (int i = 0; i < info.length; i++) {
-                    if (info[i].getState() == NetworkInfo.State.CONNECTED) {
-                        if (Constants.LOGVV) {
-                            Log.v(Constants.TAG, "network is available");
-                        }
-                        return true;
-                    }
-                }
+        for (File testDir : whitelist) {
+            if (FileUtils.contains(testDir, file)) {
+                return true;
             }
         }
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "network is not available");
-        }
+
         return false;
     }
 
-    /**
-     * Returns whether the network is roaming
-     */
-    public static boolean isNetworkRoaming(Context context) {
-        ConnectivityManager connectivity =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        if (connectivity == null) {
-            Log.w(Constants.TAG, "couldn't get connectivity manager");
-        } else {
-            NetworkInfo info = connectivity.getActiveNetworkInfo();
-            if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE) {
-                if (TelephonyManager.getDefault().isNetworkRoaming()) {
-                    if (Constants.LOGVV) {
-                        Log.v(Constants.TAG, "network is roaming");
-                    }
-                    return true;
+    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 {
-                    if (Constants.LOGVV) {
-                        Log.v(Constants.TAG, "network is not roaming");
-                    }
+                    return context.getCacheDir();
                 }
-            } else {
-                if (Constants.LOGVV) {
-                    Log.v(Constants.TAG, "not using mobile network");
+
+            case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
+                if (running) {
+                    return new File(Environment.getDownloadCacheDirectory(),
+                            Constants.DIRECTORY_CACHE_RUNNING);
+                } else {
+                    return Environment.getDownloadCacheDirectory();
                 }
-            }
-        }
-        return false;
-    }
 
-    /**
-     * Checks whether the filename looks legitimate
-     */
-    public static boolean isFilenameValid(String filename) {
-        File dir = new File(filename).getParentFile();
-        return dir.equals(Environment.getDownloadCacheDirectory())
-                || dir.equals(new File(Environment.getExternalStorageDirectory()
-                        + Constants.DEFAULT_DL_SUBDIR));
+            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);
+        }
     }
 
     /**
@@ -534,7 +410,7 @@ public class Helpers {
      */
     public static void validateSelection(String selection, Set<String> allowedColumns) {
         try {
-            if (selection == null) {
+            if (selection == null || selection.isEmpty()) {
                 return;
             }
             Lexer lexer = new Lexer(selection, allowedColumns);
@@ -545,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;
@@ -773,7 +649,7 @@ public class Helpers {
             }
 
             // anything we don't recognize
-            throw new IllegalArgumentException("illegal character");
+            throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
         }
 
         private static final boolean isIdentifierStart(char c) {
@@ -789,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();
+    }
 }