Snap for 8146118 from 9c855c73fe0a74f82fe2f08ab34d021c1e5e27fc to mainline-permission-release
Change-Id: Ifb83f993ef4527b08dca181ba663d3ad89a8c2df
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 3ed7cc0..4c8d30a 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -29,7 +29,7 @@
<string name="permlab_downloadWithoutNotification" msgid="4877101864770265405">"నోటిఫికేషన్ లేకుండానే ఫైళ్లను డౌన్లోడ్ చేయడం"</string>
<string name="permdesc_downloadWithoutNotification" msgid="7699189763226483523">"వినియోగదారుకి ఎటువంటి నోటిఫికేషన్ను చూపకుండానే డౌన్లోడ్ నిర్వాహికి ద్వారా ఫైళ్లను డౌన్లోడ్ చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_accessAllDownloads" msgid="8227356876527248611">"అన్ని సిస్టమ్ డౌన్లోడ్లను యాక్సెస్ చేయి"</string>
- <string name="permdesc_accessAllDownloads" msgid="7541731738152145079">"సిస్టమ్లో ఏదైనా యాప్ ద్వారా ప్రారంభించబడిన అన్ని డౌన్లోడ్లను వీక్షించడానికి మరియు సవరించడానికి యాప్ను అనుమతిస్తుంది."</string>
+ <string name="permdesc_accessAllDownloads" msgid="7541731738152145079">"సిస్టమ్లో ఏదైనా యాప్ ద్వారా ప్రారంభించబడిన అన్ని డౌన్లోడ్లను వీక్షించడానికి మరియు ఎడిట్ చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="download_unknown_title" msgid="1017800350818840396">"<శీర్షిక లేనిది>"</string>
<string name="notification_download_complete" msgid="466652037490092787">"డౌన్లోడ్ పూర్తయింది."</string>
<string name="notification_download_failed" msgid="3932167763860605874">"డౌన్లోడ్ విఫలమైంది."</string>
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"));