From 2d11bafb9e036129c1319794ff48be106e148e10 Mon Sep 17 00:00:00 2001 From: Dipankar Bhardwaj Date: Fri, 20 Jun 2025 15:15:29 +0000 Subject: [SP 2025-09-01] Fix misuse of MediaProvider#INCLUDED_DEFAULT_DIRECTORIES MediaProvider#INCLUDED_DEFAULT_DIRECTORIES is internally used to grant access for file rename based on relative path. It can be misused by setting it in the bundle. Updated the code to stop using this extra and stope respecting it in AccessChecker class. Test: atest MediaProviderTests Bug: 415783046 Flag: EXEMPT bug fix Change-Id: I91c44428dc730be4cf42b3cb9c02e8eca4440f81 (cherry picked from commit a631dca1a8c02d39d9a578ce331a7b23bf7ef962) Merged-In: I91c44428dc730be4cf42b3cb9c02e8eca4440f81 --- src/com/android/providers/media/AccessChecker.java | 24 +++-- src/com/android/providers/media/MediaProvider.java | 115 +++++++++++---------- .../android/providers/media/AccessCheckerTest.java | 66 +++++++++--- 3 files changed, 120 insertions(+), 85 deletions(-) diff --git a/src/com/android/providers/media/AccessChecker.java b/src/com/android/providers/media/AccessChecker.java index ba16cdf77..50a4895e0 100644 --- a/src/com/android/providers/media/AccessChecker.java +++ b/src/com/android/providers/media/AccessChecker.java @@ -55,10 +55,8 @@ import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID; import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS; import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID; import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; -import static com.android.providers.media.MediaProvider.INCLUDED_DEFAULT_DIRECTORIES; import static com.android.providers.media.util.DatabaseUtils.bindSelection; -import android.os.Bundle; import android.provider.MediaStore; import android.provider.MediaStore.Files.FileColumns; import android.provider.MediaStore.MediaColumns; @@ -69,6 +67,8 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.util.ArrayList; +import java.util.List; +import java.util.Optional; /** * Class responsible for performing all access checks (read/write access states for calling package) @@ -271,7 +271,7 @@ public class AccessChecker { @NonNull public static String getWhereForConstrainedAccess( @NonNull LocalCallingIdentity callingIdentity, int uriType, - boolean forWrite, @NonNull Bundle extras) { + boolean forWrite, Optional> includedDefaultDirectoriesOptional) { switch (uriType) { case AUDIO_MEDIA_ID: case AUDIO_MEDIA: { @@ -365,9 +365,12 @@ public class AccessChecker { // Allow access to file in directories. This si particularly used only for // SystemGallery use-case - final String defaultDirectorySql = getWhereForDefaultDirectoryMatch(extras); - if (defaultDirectorySql != null) { - options.add(defaultDirectorySql); + if (includedDefaultDirectoriesOptional.isPresent()) { + final String defaultDirectorySql = getWhereForDefaultDirectoryMatch( + includedDefaultDirectoriesOptional.get()); + if (defaultDirectorySql != null) { + options.add(defaultDirectorySql); + } } return TextUtils.join(" OR ", options); @@ -445,12 +448,11 @@ public class AccessChecker { * @see MediaProvider#INCLUDED_DEFAULT_DIRECTORIES */ @Nullable - private static String getWhereForDefaultDirectoryMatch(@NonNull Bundle extras) { - final ArrayList includedDefaultDirs = extras.getStringArrayList( - INCLUDED_DEFAULT_DIRECTORIES); + private static String getWhereForDefaultDirectoryMatch( + List includedDefaultDirectories) { final ArrayList options = new ArrayList<>(); - if (includedDefaultDirs != null) { - for (String defaultDir : includedDefaultDirs) { + if (includedDefaultDirectories != null) { + for (String defaultDir : includedDefaultDirectories) { options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'"); } } diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java index be405f160..7b9753c8d 100644 --- a/src/com/android/providers/media/MediaProvider.java +++ b/src/com/android/providers/media/MediaProvider.java @@ -443,15 +443,6 @@ public class MediaProvider extends ContentProvider { */ private static final String FILE_DATABASE_UUID = ".database_uuid"; - /** - * Specify what default directories the caller gets full access to. By default, the caller - * shouldn't get full access to any default dirs. - * But for example, we do an exception for System Gallery apps and allow them full access to: - * DCIM, Pictures, Movies. - */ - static final String INCLUDED_DEFAULT_DIRECTORIES = - "android:included-default-directories"; - /** * Value indicating that operations should include database rows matching the criteria defined * by this key only when calling package has write permission to the database row or column is @@ -3119,7 +3110,8 @@ public class MediaProvider extends ContentProvider { } final String writeAccessCheckSql = getWhereForConstrainedAccess(mCallingIdentity.get(), - uriType, /* forWrite */ true, Bundle.EMPTY); + uriType, /* forWrite */ true, /* includedDefaultDirectoriesOptional */ + Optional.empty()); final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND (%s OR %s))", column, column, MATCH_PENDING_FROM_FUSE, writeAccessCheckSql); @@ -3278,7 +3270,8 @@ public class MediaProvider extends ContentProvider { final String[] selectionArgs = new String[] {path}; final SQLiteQueryBuilder qbForQuery = - getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null); + getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, + null, /* includedDefaultDirectoriesOptional */ Optional.empty()); try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME}, selection, selectionArgs, null, null, null, null, null)) { if (!c.moveToFirst()) { @@ -3295,7 +3288,8 @@ public class MediaProvider extends ContentProvider { } final SQLiteQueryBuilder qbForUpdate = - getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null); + getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); ContentValues values = new ContentValues(); values.put(FileColumns.OWNER_PACKAGE_NAME, "null"); return qbForUpdate.update(helper, values, selection, selectionArgs) == 1; @@ -3303,14 +3297,15 @@ public class MediaProvider extends ContentProvider { private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) { - return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY); + return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY, + /* includedDefaultDirectoriesOptional */ Optional.empty()); } private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, - @NonNull Bundle qbExtras) { + @NonNull Bundle qbExtras, Optional> includedDefaultDirectoriesOptional) { return updateDatabaseForFuseRename(helper, oldPath, newPath, values, qbExtras, - FileUtils.getContentUriForPath(oldPath)); + FileUtils.getContentUriForPath(oldPath), includedDefaultDirectoriesOptional); } /** @@ -3318,10 +3313,12 @@ public class MediaProvider extends ContentProvider { */ private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, - @NonNull Bundle qbExtras, Uri uriOldPath) { + @NonNull Bundle qbExtras, Uri uriOldPath, + Optional> includedDefaultDirectoriesOptional) { boolean allowHidden = isCallingPackageAllowedHidden(); final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, - matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null); + matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null, + includedDefaultDirectoriesOptional); // uriOldPath may use Files uri which doesn't allow modifying AudioColumns. Include // AudioColumns projection map if we are modifying any audio columns while renaming @@ -3358,7 +3355,8 @@ public class MediaProvider extends ContentProvider { } if (retryUpdateWithReplace) { - if (deleteForFuseRename(helper, oldPath, newPath, qbExtras, selection, allowHidden)) { + if (deleteForFuseRename(helper, oldPath, newPath, qbExtras, selection, allowHidden, + includedDefaultDirectoriesOptional)) { Log.i(TAG, "Retrying database update after deleting conflicting entry"); count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); } else { @@ -3369,13 +3367,15 @@ public class MediaProvider extends ContentProvider { } private boolean deleteForFuseRename(DatabaseHelper helper, String oldPath, - String newPath, Bundle qbExtras, String selection, boolean allowHidden) { + String newPath, Bundle qbExtras, String selection, boolean allowHidden, + Optional> includedDefaultDirectoriesOptional) { // We are replacing file in newPath with file in oldPath. If calling package has // write permission for newPath, delete existing database entry and retry update. final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath); final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE, - matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null); - if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) { + matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null, + includedDefaultDirectoriesOptional); + if (qbForDelete.delete(helper, selection, new String[]{newPath}) == 1) { return true; } // Check if delete can be done using other URI grants @@ -3505,7 +3505,7 @@ public class MediaProvider extends ContentProvider { final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY, - null); + null, /* includedDefaultDirectoriesOptional */ Optional.empty()); final DatabaseHelper helper; try { helper = getDatabaseForUri(uriOldPath); @@ -3597,9 +3597,6 @@ public class MediaProvider extends ContentProvider { helper.beginTransaction(); try { - final Bundle qbExtras = new Bundle(); - qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES, - getIncludedDefaultDirectories()); final boolean wasHidden = FileUtils.shouldDirBeHidden(new File(oldPath)); final boolean isHidden = FileUtils.shouldDirBeHidden(new File(newPath)); for (String filePath : fileList) { @@ -3608,7 +3605,7 @@ public class MediaProvider extends ContentProvider { if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath, getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden, /* isSameMimeType */ true), - qbExtras)) { + new Bundle(), Optional.of(getIncludedDefaultDirectories()))) { Log.e(TAG, "Calling package doesn't have write permission to rename file."); return OsConstants.EPERM; } @@ -3732,7 +3729,7 @@ public class MediaProvider extends ContentProvider { return false; } return updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues, Bundle.EMPTY, - oldPathGrantedUri); + oldPathGrantedUri, /* includedDefaultDirectoriesOptional */ Optional.empty()); } /** @@ -3922,7 +3919,8 @@ public class MediaProvider extends ContentProvider { type = TYPE_QUERY; } - final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null); + final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, + null, /* includedDefaultDirectoriesOptional */ Optional.empty()); try (Cursor c = qb.query(helper, new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) { if (c.getCount() == 1) { @@ -4012,9 +4010,6 @@ public class MediaProvider extends ContentProvider { PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); queryArgs = (queryArgs != null) ? queryArgs : new Bundle(); - // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. - queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES); - final ArraySet honoredArgs = new ArraySet<>(); DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator); @@ -4081,7 +4076,7 @@ public class MediaProvider extends ContentProvider { final DatabaseHelper helper = getDatabaseForUri(uri); final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs, - honoredArgs::add); + honoredArgs::add, /* includedDefaultDirectoriesOptional */ Optional.empty()); // Allowing hidden column _user_id for this query to support Cloned Profile use case. if (table == FILES) { qb.allowColumn(FileColumns._USER_ID); @@ -5627,7 +5622,7 @@ public class MediaProvider extends ContentProvider { // row irrespective of is_download=1. final Uri uri = FileUtils.getContentUriForPath(path); SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri, - extras, null); + extras, null, /* includedDefaultDirectoriesOptional */ Optional.empty()); // We won't be able to update columns that are not part of projection map of Files table. We // have already checked strict columns in previous insert operation which failed with @@ -5712,9 +5707,6 @@ public class MediaProvider extends ContentProvider { // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. extras.remove(QUERY_ARG_REDACTED_URI); - // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. - extras.remove(INCLUDED_DEFAULT_DIRECTORIES); - final boolean allowHidden = isCallingPackageAllowedHidden(); final int match = matchUri(uri, allowHidden); @@ -5870,7 +5862,8 @@ public class MediaProvider extends ContentProvider { long rowId = -1; Uri newUri = null; - final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null); + final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); switch (match) { case IMAGES_MEDIA: { @@ -6142,7 +6135,8 @@ public class MediaProvider extends ContentProvider { // We already handle the required permission checks for the app before we get here final LocalCallingIdentity token = clearLocalCallingIdentity(); try { - return getQueryBuilder(type, match, uri, extras, honored); + return getQueryBuilder(type, match, uri, extras, honored, + /* includedDefaultDirectoriesOptional */ Optional.empty()); } finally { restoreLocalCallingIdentity(token); } @@ -6164,17 +6158,20 @@ public class MediaProvider extends ContentProvider { * */ private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match, - @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer honored) { + @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer honored, + Optional> includedDefaultDirectoriesOptional) { Trace.beginSection("MP.getQueryBuilder"); try { - return getQueryBuilderInternal(type, match, uri, extras, honored); + return getQueryBuilderInternal(type, match, uri, extras, honored, + includedDefaultDirectoriesOptional); } finally { Trace.endSection(); } } private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match, - @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer honored) { + @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer honored, + Optional> includedDefaultDirectoriesOptional) { final boolean forWrite; switch (type) { case TYPE_QUERY: forWrite = false; break; @@ -6244,7 +6241,8 @@ public class MediaProvider extends ContentProvider { // to commit to this as an API. final boolean includeAllVolumes = shouldIncludeRecentlyUnmountedVolumes(uri, extras); - appendAccessCheckQuery(qb, forWrite, uri, match, extras, volumeName); + appendAccessCheckQuery(qb, forWrite, uri, match, extras, volumeName, + includedDefaultDirectoriesOptional); switch (match) { case IMAGES_MEDIA_ID: @@ -6696,7 +6694,8 @@ public class MediaProvider extends ContentProvider { } private void appendAccessCheckQuery(@NonNull SQLiteQueryBuilder qb, boolean forWrite, - @NonNull Uri uri, int uriType, @NonNull Bundle extras, @NonNull String volumeName) { + @NonNull Uri uri, int uriType, @NonNull Bundle extras, @NonNull String volumeName, + Optional> includedDefaultDirectoriesOptional) { Objects.requireNonNull(extras); final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI); @@ -6737,7 +6736,7 @@ public class MediaProvider extends ContentProvider { // Allow access to files which are owned by the caller. Or allow access to files // based on legacy or any other special access permissions. options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite, - extras)); + includedDefaultDirectoriesOptional)); } } else { if (isLatestSelectionOnlyRequired) { @@ -6747,7 +6746,7 @@ public class MediaProvider extends ContentProvider { // Allow access to files which are owned by the caller. Or allow access to files // based on legacy or any other special access permissions. options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite, - extras)); + includedDefaultDirectoriesOptional)); } appendWhereStandalone(qb, TextUtils.join(" OR ", options)); @@ -6841,9 +6840,6 @@ public class MediaProvider extends ContentProvider { return 0; } - // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. - extras.remove(INCLUDED_DEFAULT_DIRECTORIES); - uri = safeUncanonicalize(uri); final boolean allowHidden = isCallingPackageAllowedHidden(); final int match = matchUri(uri, allowHidden); @@ -6919,7 +6915,8 @@ public class MediaProvider extends ContentProvider { } } - final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null); + final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); { // Give callers interacting with a specific media item a chance to @@ -7057,7 +7054,8 @@ public class MediaProvider extends ContentProvider { // 2. delete file row from the db final boolean allowHidden = isCallingPackageAllowedHidden(); final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, - matchUri(uriGranted, allowHidden), uriGranted, extras, null); + matchUri(uriGranted, allowHidden), uriGranted, extras, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); int count = qb.delete(helper, BaseColumns._ID + "=" + id, null); if (isDownload == 1) { @@ -8698,8 +8696,6 @@ public class MediaProvider extends ContentProvider { // Related items are only considered for new media creation, and they // can't be leveraged to move existing content into blocked locations extras.remove(QUERY_ARG_RELATED_URI); - // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. - extras.remove(INCLUDED_DEFAULT_DIRECTORIES); final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); @@ -8772,7 +8768,8 @@ public class MediaProvider extends ContentProvider { } } - final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null); + final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); // Give callers interacting with a specific media item a chance to // escalate access if they don't already have it @@ -9310,7 +9307,8 @@ public class MediaProvider extends ContentProvider { extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE, - matchUri(uri, allowHidden), uri, extras, null); + matchUri(uri, allowHidden), uri, extras, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path, mCallingIdentity.get().getSharedPackagesAsString()); @@ -9606,7 +9604,8 @@ public class MediaProvider extends ContentProvider { try { helper = getDatabaseForUri(membersUri); qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS, - membersUri, queryArgs, null); + membersUri, queryArgs, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); } catch (VolumeNotFoundException ignored) { return new int[0]; } @@ -11685,7 +11684,8 @@ public class MediaProvider extends ContentProvider { // First, check to see if caller has direct write access if (forWrite) { - final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null); + final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, + null, /* includedDefaultDirectoriesOptional */ Optional.empty()); qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN); try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN }, selection, selectionArgs, null, null, null, null, null)) { @@ -11709,7 +11709,8 @@ public class MediaProvider extends ContentProvider { } // Second, check to see if caller has direct read access - final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null); + final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null, + /* includedDefaultDirectoriesOptional */ Optional.empty()); qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN); try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN }, selection, selectionArgs, null, null, null, null, null)) { diff --git a/tests/src/com/android/providers/media/AccessCheckerTest.java b/tests/src/com/android/providers/media/AccessCheckerTest.java index 677feb775..17286c93d 100644 --- a/tests/src/com/android/providers/media/AccessCheckerTest.java +++ b/tests/src/com/android/providers/media/AccessCheckerTest.java @@ -51,7 +51,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; import android.system.Os; import android.text.TextUtils; @@ -63,6 +64,8 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Optional; @RunWith(AndroidJUnit4.class) public class AccessCheckerTest { @@ -364,22 +367,27 @@ public class AccessCheckerTest { // App with no permissions only has access to owned files assertWithMessage("Expected owned access SQL for Audio collection") - .that(getWhereForConstrainedAccess(hasNoPerms, AUDIO_MEDIA, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasNoPerms, AUDIO_MEDIA, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms) + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"); assertWithMessage("Expected owned access SQL for Video collection") - .that(getWhereForConstrainedAccess(hasNoPerms, VIDEO_MEDIA, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasNoPerms, VIDEO_MEDIA, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms)); assertWithMessage("Expected owned access SQL for Images collection") - .that(getWhereForConstrainedAccess(hasNoPerms, IMAGES_MEDIA, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasNoPerms, IMAGES_MEDIA, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms)); // App with no permissions only has access to owned files assertWithMessage("Expected owned access SQL for Downloads collection") - .that(getWhereForConstrainedAccess(hasNoPerms, DOWNLOADS, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasNoPerms, DOWNLOADS, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms)); assertWithMessage("Expected owned access SQL for FILES collection") - .that(getWhereForConstrainedAccess(hasNoPerms, FILES, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasNoPerms, FILES, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms)); } @@ -394,12 +402,27 @@ public class AccessCheckerTest { // App with READ_EXTERNAL_STORAGE or READ_MEDIA_* permission has access to only owned // non-media files or media files. assertWithMessage("Expected owned access SQL for Downloads collection") - .that(getWhereForConstrainedAccess(hasReadMedia, DOWNLOADS, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasReadMedia, DOWNLOADS, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasReadMedia)); assertWithMessage("Expected owned access SQL for FILES collection") - .that(getWhereForConstrainedAccess(hasReadMedia, FILES, false, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasReadMedia, FILES, false, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo( getWhereForOwnerPackageMatch(hasReadMedia) + " OR " + getFilesAccessSql()); + assertWithMessage("Expected owned access SQL for FILES collection") + .that(getWhereForConstrainedAccess(hasReadMedia, FILES, false, + /* includedDefaultDirectoriesOptional */Optional.of( + List.of(Environment.DIRECTORY_DCIM, Environment.DIRECTORY_PICTURES, + Environment.DIRECTORY_MOVIES)))) + .isEqualTo( + getWhereForOwnerPackageMatch(hasReadMedia) + " OR " + + getFilesAccessSql() + " OR " + + MediaStore.Files.FileColumns.RELATIVE_PATH + " LIKE 'DCIM/%'" + + " OR " + MediaStore.Files.FileColumns.RELATIVE_PATH + + " LIKE 'Pictures/%'" + + " OR " + MediaStore.Files.FileColumns.RELATIVE_PATH + + " LIKE 'Movies/%'"); } @Test @@ -409,22 +432,27 @@ public class AccessCheckerTest { // App with no permissions only has access to owned files. assertWithMessage("Expected owned access SQL for Audio collection") - .that(getWhereForConstrainedAccess(noPerms, AUDIO_MEDIA, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(noPerms, AUDIO_MEDIA, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(noPerms) + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"); assertWithMessage("Expected owned access SQL for Video collection") - .that(getWhereForConstrainedAccess(noPerms, VIDEO_MEDIA, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(noPerms, VIDEO_MEDIA, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(noPerms)); assertWithMessage("Expected owned access SQL for Images collection") - .that(getWhereForConstrainedAccess(noPerms, IMAGES_MEDIA, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(noPerms, IMAGES_MEDIA, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(noPerms)); // App with no permissions only has access to owned files assertWithMessage("Expected owned access SQL for Downloads collection") - .that(getWhereForConstrainedAccess(noPerms, DOWNLOADS, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(noPerms, DOWNLOADS, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(noPerms)); assertWithMessage("Expected owned access SQL for FILES collection") - .that(getWhereForConstrainedAccess(noPerms, FILES, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(noPerms, FILES, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(noPerms)); } @@ -439,10 +467,12 @@ public class AccessCheckerTest { // App with write permission to media files has access write access to media files and owned // files. assertWithMessage("Expected owned access SQL for Downloads collection") - .that(getWhereForConstrainedAccess(hasReadPerms, DOWNLOADS, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasReadPerms, DOWNLOADS, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasReadPerms)); assertWithMessage("Expected owned access SQL for FILES collection") - .that(getWhereForConstrainedAccess(hasReadPerms, FILES, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasReadPerms, FILES, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasReadPerms) + " OR " + getFilesAccessSql()); } @@ -481,11 +511,13 @@ public class AccessCheckerTest { // Legacy app with WRITE_EXTERNAL_STORAGE permission has access to non-media files as well. // However, they don't have global write access to secondary volume. assertWithMessage("Expected where clause SQL for Downloads collection to be") - .that(getWhereForConstrainedAccess(hasLegacyWrite, DOWNLOADS, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasLegacyWrite, DOWNLOADS, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasLegacyWrite) + " OR " + AccessChecker.getWhereForExternalPrimaryMatch()); assertWithMessage("Expected where clause SQL for FILES collection to be") - .that(getWhereForConstrainedAccess(hasLegacyWrite, FILES, true, Bundle.EMPTY)) + .that(getWhereForConstrainedAccess(hasLegacyWrite, FILES, true, + /* includedDefaultDirectoriesOptional */ Optional.empty())) .isEqualTo(getWhereForOwnerPackageMatch(hasLegacyWrite) + " OR " + AccessChecker.getWhereForExternalPrimaryMatch() + " OR " + getFilesAccessSql()); -- cgit v1.2.3-59-g8ed1b