Restrict legacy apps to insert files to other private app dirs

We allow legacy apps to insert/update content values such that they can
add private files to MediaStore database. Restrict such apps to
insert/update content in other app's private directories.
Retrict System Gallery apps and apps with MANAGE_EXTERNAL_STORAGE
permission to insert private path files.

Bug: 201667614
Test: atest FileUtilsTest
Test: atest CtsScopedStorageHostTest
Test: atest LegacyStorageTest:LegacyStorageHostTest
Change-Id: I52481b24b137790d49845f1a4b00978c63ed183b
Merged-In: I52481b24b137790d49845f1a4b00978c63ed183b
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 7196303..929e3ab 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -66,18 +66,20 @@
 import static com.android.providers.media.util.FileUtils.extractDisplayName;
 import static com.android.providers.media.util.FileUtils.extractFileExtension;
 import static com.android.providers.media.util.FileUtils.extractFileName;
+import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
 import static com.android.providers.media.util.FileUtils.extractRelativePath;
-import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
+import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
 import static com.android.providers.media.util.FileUtils.extractVolumeName;
 import static com.android.providers.media.util.FileUtils.extractVolumePath;
 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
 import static com.android.providers.media.util.FileUtils.isCrossUserEnabled;
 import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
 import static com.android.providers.media.util.FileUtils.isDownload;
 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory;
-import static com.android.providers.media.util.FileUtils.isObbOrChildPath;
+import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
 import static com.android.providers.media.util.FileUtils.sanitizePath;
 import static com.android.providers.media.util.Logging.LOGV;
 import static com.android.providers.media.util.Logging.TAG;
@@ -294,7 +296,6 @@
 
     private static final String DIRECTORY_MEDIA = "media";
     private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
-    private static final List<String> PRIVATE_SUBDIRECTORIES_ANDROID = Arrays.asList("data", "obb");
     private static final String REDACTED_URI_ID_PREFIX = "RUID";
     private static final String TRANSFORMS_SYNTHETIC_DIR = ".transforms/synthetic";
     private static final String REDACTED_URI_DIR = TRANSFORMS_SYNTHETIC_DIR + "/redacted";
@@ -1924,7 +1925,7 @@
             }
 
             // Get relative path for the contents of given directory.
-            String relativePath = extractRelativePathForDirectory(path);
+            String relativePath = extractRelativePathWithDisplayName(path);
 
             if (relativePath == null) {
                 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't
@@ -2179,8 +2180,8 @@
             throws IllegalArgumentException {
         // Try a simple check to see if the caller has full access to the given collections first
         // before falling back to performing a query to probe for access.
-        final String oldRelativePath = extractRelativePathForDirectory(oldPath);
-        final String newRelativePath = extractRelativePathForDirectory(newPath);
+        final String oldRelativePath = extractRelativePathWithDisplayName(oldPath);
+        final String newRelativePath = extractRelativePathWithDisplayName(newPath);
         boolean hasFullAccessToOldPath = false;
         boolean hasFullAccessToNewPath = false;
         for (String defaultDir : getIncludedDefaultDirectories()) {
@@ -3416,37 +3417,41 @@
     }
 
     /**
-     * Check that values don't contain any external private path.
-     * NOTE: The checks are gated on targetSDK S.
+     * For apps targetSdk >= S: Check that values does not contain any external private path.
+     * For all apps: Check that values does not contain any other app's external private paths.
      */
     private void assertPrivatePathNotInValues(ContentValues values)
             throws IllegalArgumentException {
-        if (!CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
-                Binder.getCallingUid())) {
-            // For legacy apps, let the behaviour be as it is.
-            return;
-        }
-
         ArrayList<String> relativePaths = new ArrayList<String>();
         relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA)));
         relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH));
-        /**
-         * Don't allow apps to insert/update database row to files in Android/data or
-         * Android/obb dirs. These are app private directories and files in these private
-         * directories can't be added to public media collection.
-         */
+
         for (final String relativePath : relativePaths) {
-            if (relativePath == null) continue;
+            if (!isDataOrObbRelativePath(relativePath)) {
+                continue;
+            }
 
-            final String[] relativePathSegments = relativePath.split("/", 3);
-            final String primary =
-                    (relativePathSegments.length > 0) ? relativePathSegments[0] : null;
-            final String secondary =
-                    (relativePathSegments.length > 1) ? relativePathSegments[1] : "";
+            /**
+             * Don't allow apps to insert/update database row to files in Android/data or
+             * Android/obb dirs. These are app private directories and files in these private
+             * directories can't be added to public media collection.
+             *
+             * Note: For backwards compatibility we allow apps with targetSdk < S to insert private
+             * files to MediaProvider
+             */
+            if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
+                    Binder.getCallingUid())) {
+                throw new IllegalArgumentException(
+                        "Inserting private file: " + relativePath + " is not allowed.");
+            }
 
-            if (DIRECTORY_ANDROID_LOWER_CASE.equalsIgnoreCase(primary)
-                    && PRIVATE_SUBDIRECTORIES_ANDROID.contains(
-                    secondary.toLowerCase(Locale.ROOT))) {
+            /**
+             * Restrict all (legacy and non-legacy) apps from inserting paths in other
+             * app's private directories.
+             * Allow legacy apps to insert/update files in app private directories for backward
+             * compatibility but don't allow them to do so in other app's private directories.
+             */
+            if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) {
                 throw new IllegalArgumentException(
                         "Inserting private file: " + relativePath + " is not allowed.");
             }
@@ -8688,25 +8693,28 @@
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
         try {
-            // Files under the apps own private directory
-            final String appSpecificDir = extractPathOwnerPackageName(path);
-
-            if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
-                return true;
-            }
-            // This is a private-package path; return true if accessible by the caller
-            return isUidAllowedSpecialPrivatePathAccess(uid, path);
+            return isCallingIdentityAllowedAccessToDataOrObbPath(
+                    extractRelativePathWithDisplayName(path));
         } finally {
             restoreLocalCallingIdentity(token);
         }
     }
 
+    private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) {
+        // Files under the apps own private directory
+        final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath);
+
+        if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
+            return true;
+        }
+        // This is a private-package relativePath; return true if accessible by the caller
+        return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath);
+    }
+
     /**
      * @return true iff the caller has installer privileges which gives write access to obb dirs.
-     * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
-     * package.
      */
-    private boolean isCallingIdentityAllowedInstallerAccess(int uid) {
+    private boolean isCallingIdentityAllowedInstallerAccess() {
         final boolean hasWrite = mCallingIdentity.get().
                 hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE);
 
@@ -8755,15 +8763,15 @@
         return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
     }
 
-    private boolean isCallingIdentityDownloadProvider(int uid) {
-        return uid == mDownloadsAuthorityAppId;
+    private boolean isCallingIdentityDownloadProvider() {
+        return getCallingUidOrSelf() == mDownloadsAuthorityAppId;
     }
 
-    private boolean isCallingIdentityExternalStorageProvider(int uid) {
-        return uid == mExternalStorageAuthorityAppId;
+    private boolean isCallingIdentityExternalStorageProvider() {
+        return getCallingUidOrSelf() == mExternalStorageAuthorityAppId;
     }
 
-    private boolean isCallingIdentityMtp(int uid) {
+    private boolean isCallingIdentityMtp() {
         return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP);
     }
 
@@ -8777,30 +8785,25 @@
      *
      * Installer apps can only access private-app directories on Android/obb.
      *
-     * @param uid UID of the calling package
-     * @param path the path of the file to access
+     * @param relativePath the relative path of the file to access
      */
-    private boolean isUidAllowedSpecialPrivatePathAccess(int uid, String path) {
-        final LocalCallingIdentity token =
-            clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
-        try {
-            if (SdkLevel.isAtLeastS()) {
-                return isMountModeAllowedPrivatePathAccess(uid, getCallingPackage(), path);
-            } else {
-                if (isCallingIdentityDownloadProvider(uid) ||
-                        isCallingIdentityExternalStorageProvider(uid) || isCallingIdentityMtp(
-                        uid)) {
-                    return true;
-                }
-                return (isObbOrChildPath(path) && isCallingIdentityAllowedInstallerAccess(uid));
+    private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) {
+        if (SdkLevel.isAtLeastS()) {
+            return isMountModeAllowedPrivatePathAccess(getCallingUidOrSelf(), getCallingPackage(),
+                    relativePath);
+        } else {
+            if (isCallingIdentityDownloadProvider() ||
+                    isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) {
+                return true;
             }
-        } finally {
-            restoreLocalCallingIdentity(token);
+            return (isObbOrChildRelativePath(relativePath) &&
+                    isCallingIdentityAllowedInstallerAccess());
         }
     }
 
     @RequiresApi(Build.VERSION_CODES.S)
-    private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName, String path) {
+    private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName,
+            String relativePath) {
         // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access
         // mount modes.
         final CallingIdentity token = clearCallingIdentity();
@@ -8811,7 +8814,7 @@
                 case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH:
                     return true;
                 case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER:
-                    return isObbOrChildPath(path);
+                    return isObbOrChildRelativePath(relativePath);
             }
         } catch (Exception e) {
             Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e);
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index faa80b6..937e58a 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -47,7 +47,6 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
@@ -966,20 +965,33 @@
             + "Android/(?:data|media|obb)/([^/]+)(/?.*)?");
 
     /**
-     * Regex that matches Android/obb or Android/data path.
+     * Regex that matches paths in all well-known package-specific relative directory
+     * path (as defined in {@link MediaColumns#RELATIVE_PATH})
+     * and which captures the package name as the first group.
      */
-    public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
-            "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
-            + PROP_CROSS_USER_ROOT_PATTERN
-            + "Android/(?:data|obb)/?$");
+    private static final Pattern PATTERN_OWNED_RELATIVE_PATH = Pattern.compile(
+            "(?i)^Android/(?:data|media|obb)/([^/]+)(/?.*)?");
 
     /**
-     * Regex that matches Android/obb paths.
+     * Regex that matches Android/obb or Android/data path.
      */
-    public static final Pattern PATTERN_OBB_OR_CHILD_PATH = Pattern.compile(
+    private static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
             "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
             + PROP_CROSS_USER_ROOT_PATTERN
-            + "Android/(?:obb)(/?.*)");
+            + "Android/(?:data|obb)(?:/.*)?$");
+
+    /**
+     * Regex that matches Android/obb or Android/data relative path (as defined in
+     * {@link MediaColumns#RELATIVE_PATH})
+     */
+    private static final Pattern PATTERN_DATA_OR_OBB_RELATIVE_PATH = Pattern.compile(
+            "(?i)^Android/(?:data|obb)(?:/.*)?$");
+
+    /**
+     * Regex that matches Android/obb {@link MediaColumns#RELATIVE_PATH}.
+     */
+    private static final Pattern PATTERN_OBB_OR_CHILD_RELATIVE_PATH = Pattern.compile(
+            "(?i)^Android/obb(?:/.*)?$");
 
     /**
      * The recordings directory. This is used for R OS. For S OS or later,
@@ -1105,14 +1117,13 @@
     }
 
     /**
-     * Returns relative path for the directory.
+     * Returns relative path with display name.
      */
     @VisibleForTesting
-    public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) {
-        if (directoryPath == null) return null;
+    public static @Nullable String extractRelativePathWithDisplayName(@Nullable String path) {
+        if (path == null) return null;
 
-        if (directoryPath.equals("/storage/emulated") ||
-                directoryPath.equals("/storage/emulated/")) {
+        if (path.equals("/storage/emulated") || path.equals("/storage/emulated/")) {
             // This path is not reachable for MediaProvider.
             return null;
         }
@@ -1121,18 +1132,18 @@
         // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
         // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
         // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
-        if (!directoryPath.endsWith("/")) {
+        if (!path.endsWith("/")) {
             // Relative path for directory should end with "/".
-            directoryPath += "/";
+            path += "/";
         }
 
-        final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath);
+        final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path);
         if (matcher.find()) {
-            if (matcher.end() == directoryPath.length()) {
+            if (matcher.end() == path.length()) {
                 // This is the top-level directory, so relative path is "/"
                 return "/";
             }
-            return directoryPath.substring(matcher.end());
+            return path.substring(matcher.end());
         }
         return null;
     }
@@ -1142,9 +1153,17 @@
         final Matcher m = PATTERN_OWNED_PATH.matcher(path);
         if (m.matches()) {
             return m.group(1);
-        } else {
-            return null;
         }
+        return null;
+    }
+
+    public static @Nullable String extractOwnerPackageNameFromRelativePath(@Nullable String path) {
+        if (path == null) return null;
+        final Matcher m = PATTERN_OWNED_RELATIVE_PATH.matcher(path);
+        if (m.matches()) {
+            return m.group(1);
+        }
+        return null;
     }
 
     public static boolean isExternalMediaDirectory(@NonNull String path) {
@@ -1163,20 +1182,29 @@
     }
 
     /**
-     * Returns true if relative path is Android/data or Android/obb path.
+     * Returns true if path is Android/data or Android/obb path.
      */
-    public static boolean isDataOrObbPath(String path) {
+    public static boolean isDataOrObbPath(@Nullable String path) {
         if (path == null) return false;
         final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
         return m.matches();
     }
 
     /**
+     * Returns true if relative path is Android/data or Android/obb path.
+     */
+    public static boolean isDataOrObbRelativePath(@Nullable String path) {
+        if (path == null) return false;
+        final Matcher m = PATTERN_DATA_OR_OBB_RELATIVE_PATH.matcher(path);
+        return m.matches();
+    }
+
+    /**
      * Returns true if relative path is Android/obb path.
      */
-    public static boolean isObbOrChildPath(String path) {
+    public static boolean isObbOrChildRelativePath(@Nullable String path) {
         if (path == null) return false;
-        final Matcher m = PATTERN_OBB_OR_CHILD_PATH.matcher(path);
+        final Matcher m = PATTERN_OBB_OR_CHILD_RELATIVE_PATH.matcher(path);
         return m.matches();
     }
 
@@ -1444,7 +1472,7 @@
 
         // DCIM/Camera should always be visible regardless of .nomedia presence.
         if (CAMERA_RELATIVE_PATH.equalsIgnoreCase(
-                extractRelativePathForDirectory(dir.getAbsolutePath()))) {
+                extractRelativePathWithDisplayName(dir.getAbsolutePath()))) {
             nomedia.delete();
             return false;
         }
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index aa0a697..b716f4b 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -19,7 +19,7 @@
 import static com.android.providers.media.scan.MediaScannerTest.stage;
 import static com.android.providers.media.util.FileUtils.extractDisplayName;
 import static com.android.providers.media.util.FileUtils.extractRelativePath;
-import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
+import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
 import static com.android.providers.media.util.FileUtils.isDownload;
 import static com.android.providers.media.util.FileUtils.isDownloadDir;
 
@@ -1068,7 +1068,7 @@
                 "Foo.jpg",
                 "storage/Foo"
         }) {
-            assertEquals(null, FileUtils.extractRelativePathForDirectory(path));
+            assertEquals(null, FileUtils.extractRelativePathWithDisplayName(path));
         }
     }
 
@@ -1269,7 +1269,7 @@
 
     private static void assertRelativePathForDirectory(String directoryPath, String relativePath) {
         assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :")
-                .that(extractRelativePathForDirectory(directoryPath))
+                .that(extractRelativePathWithDisplayName(directoryPath))
                 .isEqualTo(relativePath);
     }
 
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index c31a6db..a8d4f6d 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -40,17 +40,24 @@
 import static com.android.providers.media.util.FileUtils.extractDisplayName;
 import static com.android.providers.media.util.FileUtils.extractFileExtension;
 import static com.android.providers.media.util.FileUtils.extractFileName;
+import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
+import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
 import static com.android.providers.media.util.FileUtils.extractRelativePath;
 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
 import static com.android.providers.media.util.FileUtils.extractVolumeName;
 import static com.android.providers.media.util.FileUtils.extractVolumePath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory;
+import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
 import static com.android.providers.media.util.FileUtils.translateModeAccessToPosix;
 import static com.android.providers.media.util.FileUtils.translateModePfdToPosix;
 import static com.android.providers.media.util.FileUtils.translateModePosixToPfd;
 import static com.android.providers.media.util.FileUtils.translateModePosixToString;
 import static com.android.providers.media.util.FileUtils.translateModeStringToPosix;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -850,6 +857,95 @@
         assertTrue(FileUtils.isDirectoryDirty(dirInDownload));
     }
 
+    @Test
+    public void testExtractPathOwnerPackageName() {
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data/foo"))
+                .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb/foo"))
+                .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media/foo"))
+                .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/data/foo"))
+                .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/obb/foo"))
+                .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media/foo"))
+                .isEqualTo("foo");
+
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data")).isNull();
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb")).isNull();
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media")).isNull();
+        assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media")).isNull();
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/Pictures/foo")).isNull();
+        assertThat(extractPathOwnerPackageName("Android/data")).isNull();
+        assertThat(extractPathOwnerPackageName("Android/obb")).isNull();
+    }
+
+    @Test
+    public void testExtractOwnerPackageNameFromRelativePath() {
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/data/foo")).isEqualTo("foo");
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/obb/foo")).isEqualTo("foo");
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/media/foo")).isEqualTo("foo");
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/media/foo.com/files"))
+                .isEqualTo("foo.com");
+
+        assertThat(extractOwnerPackageNameFromRelativePath("/storage/emulated/0/Android/data/foo"))
+                .isNull();
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/data")).isNull();
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/obb")).isNull();
+        assertThat(extractOwnerPackageNameFromRelativePath("Android/media")).isNull();
+        assertThat(extractOwnerPackageNameFromRelativePath("Pictures/foo")).isNull();
+    }
+
+    @Test
+    public void testIsDataOrObbPath() {
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/data")).isTrue();
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb")).isTrue();
+        assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data")).isTrue();
+        assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb")).isTrue();
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/data/foo")).isTrue();
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb/foo")).isTrue();
+        assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data/foo")).isTrue();
+        assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb/foo")).isTrue();
+
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/")).isFalse();
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/media/")).isFalse();
+        assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/media/")).isFalse();
+        assertThat(isDataOrObbPath("/storage/emulated/0/Pictures/")).isFalse();
+        assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obbfoo")).isFalse();
+        assertThat(isDataOrObbPath("/storage/emulated/0/Android/datafoo")).isFalse();
+        assertThat(isDataOrObbPath("Android/")).isFalse();
+        assertThat(isDataOrObbPath("Android/media/")).isFalse();
+    }
+
+    @Test
+    public void testIsDataOrObbRelativePath() {
+        assertThat(isDataOrObbRelativePath("Android/data")).isTrue();
+        assertThat(isDataOrObbRelativePath("Android/obb")).isTrue();
+        assertThat(isDataOrObbRelativePath("Android/data/foo")).isTrue();
+        assertThat(isDataOrObbRelativePath("Android/obb/foo")).isTrue();
+
+        assertThat(isDataOrObbRelativePath("/storage/emulated/0/Android/data")).isFalse();
+        assertThat(isDataOrObbRelativePath("Android/")).isFalse();
+        assertThat(isDataOrObbRelativePath("Android/media/")).isFalse();
+        assertThat(isDataOrObbRelativePath("Pictures/")).isFalse();
+    }
+
+    @Test
+    public void testIsObbOrChildRelativePath() {
+        assertThat(isObbOrChildRelativePath("Android/obb")).isTrue();
+        assertThat(isObbOrChildRelativePath("Android/obb/")).isTrue();
+        assertThat(isObbOrChildRelativePath("Android/obb/foo.com")).isTrue();
+
+        assertThat(isObbOrChildRelativePath("/storage/emulated/0/Android/obb")).isFalse();
+        assertThat(isObbOrChildRelativePath("/storage/emulated/0/Android/")).isFalse();
+        assertThat(isObbOrChildRelativePath("Android/")).isFalse();
+        assertThat(isObbOrChildRelativePath("Android/media/")).isFalse();
+        assertThat(isObbOrChildRelativePath("Pictures/")).isFalse();
+        assertThat(isObbOrChildRelativePath("Android/obbfoo")).isFalse();
+        assertThat(isObbOrChildRelativePath("Android/data")).isFalse();
+    }
+
     private File getNewDirInDownload(String name) {
         File file = new File(mTestDownloadDir, name);
         assertTrue(file.mkdir());