Snap for 8068644 from 52969f9fcec40ddb839569d97199bb6c90498a87 to sc-v2-release

Change-Id: I137ebecff2a4c1cb9fc49cf49010d50587677417
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index a9e0237..1afa090 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -257,6 +257,7 @@
     private int mSystemUid = -1;
 
     private StorageManager mStorageManager;
+    private AppOpsManager mAppOpsManager;
 
     /**
      * Creates and updated database on demand when opening it.
@@ -587,6 +588,7 @@
         mSystemUid = Process.SYSTEM_UID;
 
         mStorageManager = getContext().getSystemService(StorageManager.class);
+        mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
 
         // Grant access permissions for all known downloads to the owning apps.
         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
@@ -735,10 +737,9 @@
                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                         "No permission to write");
 
-                final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
-                if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
-                        Binder.getCallingUid(), getCallingAttributionTag(), null)
-                        != AppOpsManager.MODE_ALLOWED) {
+                if (mAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                        getCallingPackage(), Binder.getCallingUid(), getCallingAttributionTag(),
+                        null) != AppOpsManager.MODE_ALLOWED) {
                     throw new SecurityException("No permission to write");
                 }
             }
@@ -1067,41 +1068,11 @@
             throw new SecurityException(e);
         }
 
-        final int targetSdkVersion = getCallingPackageTargetSdkVersion();
-        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
-        final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
+        final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
                 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
-
-        if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
-                || Helpers.isFilenameValidInKnownPublicDir(file.getAbsolutePath())) {
-            // No permissions required for paths belonging to calling package or
-            // public downloads dir.
-            return;
-        } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) {
-            // Otherwise we require write permission
-            getContext().enforceCallingOrSelfPermission(
-                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
-                    "No permission to write to " + file);
-
-            final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
-            if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
-                    Binder.getCallingUid(), getCallingAttributionTag(), null)
-                    != AppOpsManager.MODE_ALLOWED) {
-                throw new SecurityException("No permission to write to " + file);
-            }
-        } else if (Helpers.isFilenameValidInExternalObbDir(file) &&
-                ((appOpsManager.noteOp(
-                    AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
-                    Binder.getCallingUid(), getCallingPackage(), null, "obb_download")
-                        == AppOpsManager.MODE_ALLOWED)
-                || (getContext().checkCallingOrSelfPermission(
-                    android.Manifest.permission.REQUEST_INSTALL_PACKAGES)
-                    == PackageManager.PERMISSION_GRANTED))) {
-            // Installers are allowed to download in OBB dirs, even outside their own package
-            return;
-        } else {
-            throw new SecurityException("Unsupported path " + file);
-        }
+        Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(),
+                mAppOpsManager, getCallingAttributionTag(), isLegacyMode,
+                /* allowDownloadsDirOnly */ false);
     }
 
     private void checkDownloadedFilePath(ContentValues values) {
@@ -1123,49 +1094,15 @@
             throw new IllegalArgumentException("File doesn't exist: " + file);
         }
 
-        final int targetSdkVersion = getCallingPackageTargetSdkVersion();
-        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
-        final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
-                Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
-
         if (Binder.getCallingPid() == Process.myPid()) {
             return;
-        } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
-                || Helpers.isFilenameValidInPublicDownloadsDir(file)) {
-            // No permissions required for paths belonging to calling package or
-            // public downloads dir.
-            return;
-        } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) {
-            // Otherwise we require write permission
-            getContext().enforceCallingOrSelfPermission(
-                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
-                    "No permission to write to " + file);
-
-            final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
-            if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
-                    Binder.getCallingUid(), getCallingAttributionTag(), null)
-                    != AppOpsManager.MODE_ALLOWED) {
-                throw new SecurityException("No permission to write to " + file);
-            }
-        } else {
-            throw new SecurityException("Unsupported path " + file);
         }
-    }
 
-    private int getCallingPackageTargetSdkVersion() {
-        final String callingPackage = getCallingPackage();
-        if (callingPackage != null) {
-            ApplicationInfo ai = null;
-            try {
-                ai = getContext().getPackageManager()
-                        .getApplicationInfo(callingPackage, 0);
-            } catch (PackageManager.NameNotFoundException ignored) {
-            }
-            if (ai != null) {
-                return ai.targetSdkVersion;
-            }
-        }
-        return Build.VERSION_CODES.CUR_DEVELOPMENT;
+        final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
+                Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
+        Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(),
+                mAppOpsManager, getCallingAttributionTag(), isLegacyMode,
+                /* allowDownloadsDirOnly */ true);
     }
 
     /**
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 772c0b9..574b222 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -34,6 +34,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.AppOpsManager;
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.content.ComponentName;
@@ -41,8 +42,10 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
@@ -84,6 +87,9 @@
     private static final Pattern PATTERN_ANDROID_DIRS =
             Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(?:data|obb|media)/.+");
 
+    private static final Pattern PATTERN_ANDROID_PRIVATE_DIRS =
+            Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(data|obb)/.+");
+
     private static final Pattern PATTERN_PUBLIC_DIRS =
             Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/([^/]+)/.+");
 
@@ -530,8 +536,7 @@
      * directories that are always writable to apps, regardless of storage
      * permission.
      */
-    static boolean isFilenameValidInExternalPackage(Context context, File file,
-            String packageName) {
+    static boolean isFilenameValidInExternalPackage(File file, String packageName) {
         try {
             if (containsCanonical(buildExternalStorageAppDataDirs(packageName), file) ||
                     containsCanonical(buildExternalStorageAppObbDirs(packageName), file) ||
@@ -539,7 +544,7 @@
                 return true;
             }
         } catch (IOException e) {
-            Log.w(TAG, "Failed to resolve canonical path: " + e);
+            Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
             return false;
         }
 
@@ -552,13 +557,77 @@
                 return true;
             }
         } catch (IOException e) {
-            Log.w(TAG, "Failed to resolve canonical path: " + e);
+            Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
             return false;
         }
 
         return false;
     }
 
+    /**
+     * Check if given file exists in one of the private package-specific external storage
+     * directories.
+     */
+    static boolean isFileInPrivateExternalAndroidDirs(File file) {
+        try {
+            return PATTERN_ANDROID_PRIVATE_DIRS.matcher(file.getCanonicalPath()).matches();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks destination file path restrictions adhering to App privacy restrictions
+     *
+     * Note: This method is extracted to a static method for better test coverage.
+     */
+    @VisibleForTesting
+    static void checkDestinationFilePathRestrictions(File file, String callingPackage,
+            Context context, AppOpsManager appOpsManager, String callingAttributionTag,
+            boolean isLegacyMode, boolean allowDownloadsDirOnly) {
+        boolean isFileNameValid = allowDownloadsDirOnly ? isFilenameValidInPublicDownloadsDir(file)
+                : isFilenameValidInKnownPublicDir(file.getAbsolutePath());
+        if (isFilenameValidInExternalPackage(file, callingPackage) || isFileNameValid) {
+            // No permissions required for paths belonging to calling package or
+            // public downloads dir.
+            return;
+        } else if (isFilenameValidInExternalObbDir(file) &&
+                isCallingAppInstaller(context, appOpsManager, callingPackage)) {
+            // Installers are allowed to download in OBB dirs, even outside their own package
+            return;
+        } else if (isFileInPrivateExternalAndroidDirs(file)) {
+            // Positive cases of writing to external Android dirs is covered in the if blocks above.
+            // If the caller made it this far, then it cannot write to this path as it is restricted
+            // from writing to other app's external Android dirs.
+            throw new SecurityException("Unsupported path " + file);
+        } else if (isLegacyMode && isFilenameValidInExternal(context, file)) {
+            // Otherwise we require write permission
+            context.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+                    "No permission to write to " + file);
+
+            if (appOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                    callingPackage, Binder.getCallingUid(), callingAttributionTag, null)
+                    != AppOpsManager.MODE_ALLOWED) {
+                throw new SecurityException("No permission to write to " + file);
+            }
+        } else {
+            throw new SecurityException("Unsupported path " + file);
+        }
+    }
+
+    private static boolean isCallingAppInstaller(Context context, AppOpsManager appOpsManager,
+            String callingPackage) {
+        return (appOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                Binder.getCallingUid(), callingPackage, null, "obb_download")
+                == AppOpsManager.MODE_ALLOWED)
+                || (context.checkCallingOrSelfPermission(
+                android.Manifest.permission.REQUEST_INSTALL_PACKAGES)
+                == PackageManager.PERMISSION_GRANTED);
+    }
+
     static boolean isFilenameValidInPublicDownloadsDir(File file) {
         try {
             if (containsCanonical(buildExternalStoragePublicDirs(
@@ -566,7 +635,7 @@
                 return true;
             }
         } catch (IOException e) {
-            Log.w(TAG, "Failed to resolve canonical path: " + e);
+            Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
             return false;
         }
 
@@ -608,7 +677,7 @@
                 }
             }
         } catch (IOException e) {
-            Log.w(TAG, "Failed to resolve canonical path: " + e);
+            Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
             return false;
         }
 
@@ -786,13 +855,13 @@
             final ContentValues values = new ContentValues();
             values.putNull(Constants.UID);
             downloadProvider.update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
-                    Helpers.buildQueryWithIds(idsToOrphan), null);
+                    buildQueryWithIds(idsToOrphan), null);
         }
         if (idsToDelete.size() > 0) {
             Log.i(Constants.TAG, "Deleting downloads with ids "
                     + Arrays.toString(idsToDelete.toArray()) + " as owner package is removed");
             downloadProvider.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                    Helpers.buildQueryWithIds(idsToDelete), null);
+                    buildQueryWithIds(idsToDelete), null);
         }
     }
 
diff --git a/tests/src/com/android/providers/downloads/HelpersTest.java b/tests/src/com/android/providers/downloads/HelpersTest.java
index 08c0b13..eb742d6 100644
--- a/tests/src/com/android/providers/downloads/HelpersTest.java
+++ b/tests/src/com/android/providers/downloads/HelpersTest.java
@@ -35,11 +35,13 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.content.ContentProvider;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.MatrixCursor;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Environment;
 import android.os.Process;
 import android.provider.Downloads;
@@ -49,11 +51,8 @@
 import android.util.LongArray;
 import android.util.LongSparseArray;
 
-import libcore.io.IoUtils;
-
 import java.io.File;
 import java.util.Arrays;
-import java.util.function.BiConsumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -62,6 +61,7 @@
  */
 @SmallTest
 public class HelpersTest extends AndroidTestCase {
+    private static final String TAG = "DownloadManagerHelpersTest";
 
     private final static int TEST_UID1 = 11111;
     private final static int TEST_UID2 = 11112;
@@ -72,7 +72,8 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-
+        // This is necessary for mockito to work
+        System.setProperty("dexmaker.dexcache", mContext.getCacheDir().toString());
         mMockitoHelper.setUp(getClass());
     }
 
@@ -149,6 +150,363 @@
                 "/storage/AAAA-FFFF/Download/dir/bar.html"));
     }
 
+    public void testCheckDestinationFilePathRestrictions_noPermission() throws Exception {
+        // Downloading to our own private app directory should always be allowed, even for
+        // permission-less app
+        checkDestinationFilePathRestrictions_noPermission(
+                "/storage/emulated/0/Android/data/DownloadManagerHelpersTest/test",
+                /* isLegacyMode */ false);
+        checkDestinationFilePathRestrictions_noPermission(
+                "/storage/emulated/0/Android/data/DownloadManagerHelpersTest/test",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_noPermission(
+                "/storage/emulated/0/Android/obb/DownloadManagerHelpersTest/test",
+                /* isLegacyMode */ false);
+        checkDestinationFilePathRestrictions_noPermission(
+                "/storage/emulated/0/Android/obb/DownloadManagerHelpersTest/test",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_noPermission(
+                "/storage/emulated/0/Android/media/DownloadManagerHelpersTest/test",
+                /* isLegacyMode */ false);
+        checkDestinationFilePathRestrictions_noPermission(
+                "/storage/emulated/0/Android/media/DownloadManagerHelpersTest/test",
+                /* isLegacyMode */ true);
+
+        // All apps can write to Environment.STANDARD_DIRECTORIES
+        checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Pictures/test",
+                /* isLegacyMode */ false);
+        checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Download/test",
+                /* isLegacyMode */ false);
+        checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Pictures/test",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Download/test",
+                /* isLegacyMode */ true);
+
+        // Apps can never access other app's private directories (Android/data, Android/obb) paths
+        // (unless they are installers in which case they can access Android/obb paths)
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot access other app's private packages");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot access other app's private packages"
+                    + " even in legacy mode");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/obb/foo/test", /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot access other app's private packages");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/obb/foo/test", /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot access other app's private packages"
+                    + " even in legacy mode");
+        } catch (SecurityException expected) {
+        }
+
+        // Non-legacy apps can never access Android/ or Android/media dirs for other packages.
+        try {
+            checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Android/",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/media/", /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/media/foo", /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        // Legacy apps require WRITE_EXTERNAL_STORAGE permission to access Android/ or Android/media
+        // dirs.
+        try {
+            checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Android/",
+                    /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+                    + " have WRITE_EXTERNAL_STORAGE permission");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/media/", /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+                    + " have WRITE_EXTERNAL_STORAGE permission");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_noPermission(
+                    "/storage/emulated/0/Android/media/foo", /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot write to Android/media as it does not"
+                    + " have WRITE_EXTERNAL_STORAGE permission");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    public void testCheckDestinationFilePathRestrictions_installer() throws Exception {
+        // Downloading to other obb dirs should be allowed as installer
+        checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/obb/foo/test",
+                /* isLegacyMode */ false);
+        checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/obb/foo/test",
+                /* isLegacyMode */ true);
+
+        // Installer apps can not access other app's Android/data private dirs
+        try {
+            checkDestinationFilePathRestrictions_installer(
+                    "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot access other app's private packages");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_installer(
+                    "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot access other app's private packages"
+                    + " even in legacy mode");
+        } catch (SecurityException expected) {
+        }
+
+        // Non-legacy apps can never access Android/ or Android/media dirs for other packages.
+        try {
+            checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/foo",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        // Legacy apps require WRITE_EXTERNAL_STORAGE permission to access Android/ or Android/media
+        // dirs.
+        try {
+            checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/",
+                    /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+                    + " have WRITE_EXTERNAL_STORAGE permission");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/",
+                    /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+                    + " have WRITE_EXTERNAL_STORAGE permission");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/foo",
+                    /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot write to Android/media as it does not"
+                    + " have WRITE_EXTERNAL_STORAGE permission");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    public void testCheckDestinationFilePathRestrictions_WES() throws Exception {
+        // Apps with WRITE_EXTERNAL_STORAGE can not access other app's private dirs
+        // (Android/data and Android/obb paths)
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/data/foo/test",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot access other app's private packages");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/data/foo/test",
+                    /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot access other app's private packages"
+                    + " even in legacy mode");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/obb/foo/test",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot access other app's private packages");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/obb/foo/test",
+                    /* isLegacyMode */ true);
+            fail("Expected SecurityException as caller cannot access other app's private packages"
+                    + " even in legacy mode");
+        } catch (SecurityException expected) {
+        }
+
+        // Non-legacy apps can never access Android/ or Android/media dirs for other packages.
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        try {
+            checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/foo",
+                    /* isLegacyMode */ false);
+            fail("Expected SecurityException as caller cannot write to Android dir");
+        } catch (SecurityException expected) {
+        }
+
+        // Legacy apps with WRITE_EXTERNAL_STORAGE can access shared storage file path including
+        // Android/ and Android/media dirs
+        checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Pictures/test",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Download/test",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/",
+                /* isLegacyMode */ true);
+        checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/foo",
+                /* isLegacyMode */ true);
+    }
+
+    private void checkDestinationFilePathRestrictions_noPermission(String filePath,
+            boolean isLegacyMode) {
+        final Context mockContext = mock(Context.class);
+        when(mockContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+        when(mockContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+        final String callingAttributionTag = "test";
+        final AppOpsManager mockAppOpsManager = mock(AppOpsManager.class);
+        final String callingPackage = TAG;
+        when(mockAppOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                Binder.getCallingUid(), callingPackage, null, "obb_download"))
+                .thenReturn(AppOpsManager.MODE_ERRORED);
+        when(mockAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                callingPackage, Binder.getCallingUid(), callingAttributionTag, null))
+                .thenReturn(AppOpsManager.MODE_ERRORED);
+        File file = new File(filePath);
+
+        Helpers.checkDestinationFilePathRestrictions(file, callingPackage, mockContext,
+                mockAppOpsManager, callingAttributionTag, isLegacyMode,
+                /* allowDownloadsDirOnly */ false);
+    }
+
+    private void checkDestinationFilePathRestrictions_installer(String filePath,
+            boolean isLegacyMode) throws Exception {
+        final Context mockContext = mock(Context.class);
+        when(mockContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+        when(mockContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        final String callingAttributionTag = "test";
+        final AppOpsManager mockAppOpsManager = mock(AppOpsManager.class);
+        final String callingPackage = TAG;
+        when(mockAppOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                Binder.getCallingUid(), callingPackage, null, "obb_download"))
+                .thenReturn(AppOpsManager.MODE_ALLOWED);
+        when(mockAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                callingPackage, Binder.getCallingUid(), callingAttributionTag, null))
+                .thenReturn(AppOpsManager.MODE_ERRORED);
+        File file = new File(filePath);
+
+        Helpers.checkDestinationFilePathRestrictions(file, callingPackage, mockContext,
+                mockAppOpsManager, callingAttributionTag, isLegacyMode,
+                /* allowDownloadsDirOnly */ false);
+    }
+
+    private void checkDestinationFilePathRestrictions_WES(String filePath, boolean isLegacyMode)
+            throws Exception {
+        final Context mockContext = mock(Context.class);
+        when(mockContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+        when(mockContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        final AppOpsManager mockAppOpsManager = mock(AppOpsManager.class);
+        final String callingAttributionTag = "test";
+        final String callingPackage = TAG;
+        when(mockAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                callingPackage, Binder.getCallingUid(), callingAttributionTag, null))
+                .thenReturn(AppOpsManager.MODE_ALLOWED);
+        when(mockAppOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                Binder.getCallingUid(), callingPackage, null, "obb_download"))
+                .thenReturn(AppOpsManager.MODE_ERRORED);
+        File file = new File(filePath);
+
+        Helpers.checkDestinationFilePathRestrictions(file, callingPackage, mockContext,
+                mockAppOpsManager, callingAttributionTag, isLegacyMode,
+                /* allowDownloadsDirOnly */ false);
+    }
+
+    public void testIsFileInPrivateExternalAndroidDirs() throws Exception {
+        assertTrue(isFileInPrivateExternalAndroidDirs(
+                "/storage/emulated/0/Android/data/com.example"));
+        assertTrue(isFileInPrivateExternalAndroidDirs(
+                "/storage/emulated/0/Android/data/com.example/colors.txt"));
+        assertTrue(isFileInPrivateExternalAndroidDirs(
+                "/storage/emulated/0/Android/obb/com.example/file.mp4"));
+        assertTrue(isFileInPrivateExternalAndroidDirs(
+                "/storage/AAAA-FFFF/Android/obb/com.example/file.mp4"));
+
+        assertFalse(isFileInPrivateExternalAndroidDirs("/storage/emulated/0/Android/"));
+        assertFalse(isFileInPrivateExternalAndroidDirs("/storage/AAAA-FFFF/Android/"));
+        assertFalse(isFileInPrivateExternalAndroidDirs(
+                "/storage/emulated/0/Android/media/com.example/file.mp4"));
+        assertFalse(isFileInPrivateExternalAndroidDirs(
+                "/storage/AAAA-FFFF/Android/media/com.example/file.mp4"));
+        assertFalse(isFileInPrivateExternalAndroidDirs("/storage/emulated/0/Download/foo.pdf"));
+        assertFalse(isFileInPrivateExternalAndroidDirs(
+                "/storage/emulated/0/Download/dir/bar.html"));
+        assertFalse(isFileInPrivateExternalAndroidDirs("/storage/AAAA-FFFF/Download/dir/bar.html"));
+    }
+
+    private static boolean isFileInPrivateExternalAndroidDirs(String filePath) {
+        return Helpers.isFileInPrivateExternalAndroidDirs(new File(filePath));
+    }
+
     public void testIsFilenameValidinKnownPublicDir() throws Exception {
         assertTrue(Helpers.isFilenameValidInKnownPublicDir(
                 "/storage/emulated/0/Download/dir/file.txt"));