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());