Download dir: /data/data/com.android.providers.downloads/cache NOT /cache
Vasu Nori [Tue, 14 Dec 2010 00:29:29 +0000 (16:29 -0800)]
bug:3264401
still to do:
   make sure only N bytes are taken up by downloads dir
     N = a value specific to each device.
     default = 100MB.

Change-Id: I2a49f4b3831d3a8d7be13b5fd46d85d56e831e38

src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadService.java
src/com/android/providers/downloads/DownloadThread.java
src/com/android/providers/downloads/Helpers.java
tests/public_api_access/AndroidManifest.xml
tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java

index 363b68c..d1ea43e 100644 (file)
@@ -451,6 +451,7 @@ public class DownloadInfo {
 
     public boolean isOnCache() {
         return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION
+                || mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION
                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
     }
index 9336b73..d848b65 100644 (file)
@@ -151,6 +151,7 @@ public final class DownloadProvider extends ContentProvider {
     /** List of uids that can access the downloads */
     private int mSystemUid = -1;
     private int mDefContainerUid = -1;
+    private File mDownloadsDataDir;
 
     @VisibleForTesting
     SystemFacade mSystemFacade;
@@ -421,6 +422,7 @@ public final class DownloadProvider extends ContentProvider {
         if (appInfo != null) {
             mDefContainerUid = appInfo.uid;
         }
+        mDownloadsDataDir = Helpers.getDownloadsDataDirectory(getContext());
         return true;
     }
 
@@ -490,7 +492,8 @@ public final class DownloadProvider extends ContentProvider {
                     && dest != Downloads.Impl.DESTINATION_EXTERNAL
                     && dest != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
                     && dest != Downloads.Impl.DESTINATION_FILE_URI) {
-                throw new SecurityException("unauthorized destination code");
+                throw new SecurityException("setting destination to : " + dest +
+                        " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
             }
             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
             // switch to non-purgeable download
@@ -508,6 +511,11 @@ public final class DownloadProvider extends ContentProvider {
                         Binder.getCallingPid(), Binder.getCallingUid(),
                         "need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
                 checkFileUriDestination(values);
+            } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
+                getContext().enforcePermission(
+                        android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
+                        Binder.getCallingPid(), Binder.getCallingUid(),
+                        "need ACCESS_CACHE_FILESYSTEM permission to use system cache");
             }
             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
         }
@@ -1068,7 +1076,7 @@ public final class DownloadProvider extends ContentProvider {
         if (path == null) {
             throw new FileNotFoundException("No filename found.");
         }
-        if (!Helpers.isFilenameValid(path)) {
+        if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) {
             throw new FileNotFoundException("Invalid filename.");
         }
         if (!"r".equals(mode)) {
index f93c5c2..62e355c 100644 (file)
@@ -93,10 +93,14 @@ public class DownloadService extends Service {
 
     private boolean mMediaScannerConnecting;
 
+    private static final int LOCATION_SYSTEM_CACHE = 1;
+    private static final int LOCATION_DOWNLOAD_DATA_DIR = 2;
+
     /**
      * The IPC interface to the Media Scanner
      */
     private IMediaScannerService mMediaScannerService;
+    private File mDownloadsDataDir;
 
     @VisibleForTesting
     SystemFacade mSystemFacade;
@@ -218,7 +222,7 @@ public class DownloadService extends Service {
 
         mNotifier = new DownloadNotification(this, mSystemFacade);
         mSystemFacade.cancelAllNotifications();
-
+        mDownloadsDataDir = Helpers.getDownloadsDataDirectory(getApplicationContext());
         updateFromProvider();
     }
 
@@ -267,7 +271,10 @@ public class DownloadService extends Service {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
 
             trimDatabase();
-            removeSpuriousFiles();
+            // remove spurious files from system cache
+            removeSpuriousFiles(LOCATION_SYSTEM_CACHE);
+            // remove spurious files from downloads dir
+            removeSpuriousFiles(LOCATION_DOWNLOAD_DATA_DIR);
 
             boolean keepService = false;
             // for each update from the database, remember which download is
@@ -423,10 +430,13 @@ public class DownloadService extends Service {
     }
 
     /**
-     * Removes files that may have been left behind in the cache directory
+     * Removes files that may have been left behind in the systemcache or
+     * /data/downloads directory
      */
-    private void removeSpuriousFiles() {
-        File[] files = Environment.getDownloadCacheDirectory().listFiles();
+    private void removeSpuriousFiles(int location) {
+        File base = (location == LOCATION_SYSTEM_CACHE) ?
+                Environment.getDownloadCacheDirectory() : mDownloadsDataDir;
+        File[] files = base.listFiles();
         if (files == null) {
             // The cache folder doesn't appear to exist (this is likely the case
             // when running the simulator).
index a1d9101..c497d5c 100644 (file)
@@ -437,7 +437,8 @@ public class DownloadThread extends Thread {
                 return;
             } catch (IOException ex) {
                 if (mInfo.isOnCache()) {
-                    if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
+                    if (Helpers.discardPurgeableFiles(mInfo.mDestination, mContext,
+                            Constants.BUFFER_SIZE)) {
                         continue;
                     }
                 } else if (!Helpers.isExternalMediaMounted()) {
@@ -446,7 +447,7 @@ public class DownloadThread extends Thread {
                 }
 
                 long availableBytes =
-                    Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
+                    Helpers.getAvailableBytes(Helpers.getFilesystemRoot(mContext, state.mFilename));
                 if (availableBytes < bytesRead) {
                     throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
                             "insufficient space while writing destination file", ex);
@@ -802,7 +803,8 @@ public class DownloadThread extends Thread {
     private void setupDestinationFile(State state, InnerState innerState)
             throws StopRequest {
         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
-            if (!Helpers.isFilenameValid(state.mFilename)) {
+            if (!Helpers.isFilenameValid(state.mFilename, 
+                    Helpers.getDownloadsDataDirectory(mContext))) {
                 // this should never happen
                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
                         "found invalid internal destination filename");
index 59cc97c..052876f 100644 (file)
@@ -98,7 +98,7 @@ public class Helpers {
             boolean isPublicApi) throws GenerateSaveFileError {
         checkCanHandleDownload(context, mimeType, destination, isPublicApi);
         if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
-            String path = verifyFileUri(hint, contentLength);
+            String path = verifyFileUri(context, hint, contentLength);
             String c = getFullPath(path, mimeType, destination, null);
             return c;
         } else {
@@ -107,14 +107,14 @@ public class Helpers {
         }
     }
 
-    private static String verifyFileUri(String hint, long contentLength)
+    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(path)) < contentLength) {
+        if (getAvailableBytes(getFilesystemRoot(context, path)) < contentLength) {
             throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
                     "insufficient space on external storage");
         }
@@ -125,11 +125,15 @@ public class Helpers {
     /**
      * @return the root of the filesystem containing the given path
      */
-    public static File getFilesystemRoot(String 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;
@@ -221,8 +225,9 @@ public class Helpers {
         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);
+            return getCacheDestination(context, contentLength, destination);
         }
 
         return getExternalDestination(contentLength);
@@ -261,18 +266,20 @@ public class Helpers {
         return true;
     }
 
-    private static File getCacheDestination(Context context, long contentLength)
+    private static File getCacheDestination(Context context, long contentLength, int destination)
             throws GenerateSaveFileError {
         File base;
-        base = Environment.getDownloadCacheDirectory();
+        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(context, contentLength - bytesAvailable)) {
+            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, unable to free any "
-                        + "more");
+                        "not enough free space in internal download storage: " + base +
+                        ", unable to free any more");
             }
             bytesAvailable = getAvailableBytes(base);
         }
@@ -443,6 +450,7 @@ public class Helpers {
         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;
@@ -484,15 +492,19 @@ public class Helpers {
      * the matching database entries. Files are deleted in LRU order until
      * the total byte size is greater than targetBytes.
      */
-    public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
+    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 +
-                        " = '" + Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE + "' )",
-                null,
+                Downloads.Impl.COLUMN_DESTINATION + " = '?' )",
+                bindArgs,
                 Downloads.Impl.COLUMN_LAST_MODIFICATION);
         if (cursor == null) {
             return false;
@@ -536,9 +548,10 @@ public class Helpers {
     /**
      * Checks whether the filename looks legitimate
      */
-    public static boolean isFilenameValid(String filename) {
+    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());
     }
 
@@ -851,4 +864,7 @@ public class Helpers {
         }
         return sb.toString();
     }
+    static final File getDownloadsDataDirectory(Context context) {
+        return context.getCacheDir();
+    }
 }
index 0104846..bec636b 100644 (file)
@@ -23,6 +23,9 @@
     </application>
 
     <uses-permission android:name="android.permission.INTERNET"/>
+    <!-- The tests in this package can access /cache. so they need the following permission -->
+    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED"/>
+    <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM"/>
 
     <!--
     The test declared in this instrumentation can be run via this command
index 7a2bfdf..a5bae8b 100644 (file)
@@ -69,6 +69,21 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
                          getDownloadFilename(downloadUri));
     }
 
+    /**
+     * downloading to system cache should succeed because this tests package has 
+     * the permission android.permission.ACCESS_CACHE_FILESYSTEM
+     */
+    public void testDownloadToSystemCache() throws Exception {
+        enqueueResponse(HTTP_OK, FILE_CONTENT);
+        Uri downloadUri = requestDownload("/path");
+        updateDownload(downloadUri, Downloads.Impl.COLUMN_DESTINATION,
+                       Integer.toString(Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION));
+        runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
+        assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
+        assertStartsWith(Environment.getDownloadCacheDirectory().getPath(),
+                         getDownloadFilename(downloadUri));
+    }
+
     public void testRoaming() throws Exception {
         mSystemFacade.mActiveNetworkType = ConnectivityManager.TYPE_MOBILE;
         mSystemFacade.mIsRoaming = true;