Merge "Ignore flaky test until fixed." into sc-dev
diff --git a/Android.bp b/Android.bp
index a809320..3cad281 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,8 +30,8 @@
     libs: [
         "unsupportedappusage",
         "app-compat-annotations",
+        "framework-annotations-lib",
         "framework-mediaprovider.impl",
-        "framework_mediaprovider_annotation",
         "framework-media.stubs.module_lib",
         "framework-statsd",
     ],
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index 66d9a5b..f770105 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -36,7 +36,6 @@
     installable: true,
 
     libs: [
-        "framework_mediaprovider_annotation",
         "framework-media.stubs.module_lib",
         "unsupportedappusage",
     ],
@@ -59,10 +58,3 @@
     ],
     path: "java",
 }
-
-java_library {
-    name: "framework_mediaprovider_annotation",
-    srcs: [":framework-mediaprovider-annotation-sources"],
-    installable: false,
-    sdk_version: "core_current",
-}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 9fb2a23..084f9e0 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1860,6 +1860,12 @@
             public static final int MEDIA_TYPE_DOCUMENT = 6;
 
             /**
+             * Constant indicating the count of {@link #MEDIA_TYPE} columns.
+             * @hide
+             */
+            public static final int MEDIA_TYPE_COUNT = 7;
+
+            /**
              * Modifier of the database row
              *
              * Specifies the last modifying operation of the database row. This
@@ -4255,6 +4261,9 @@
 
     /**
      * Returns true if the given application is the current system gallery of the device.
+     * <p>
+     * The system gallery is one app chosen by the OEM that has read & write access to all photos
+     * and videos on the device and control over folders in media collections.
      *
      * @param resolver The {@link ContentResolver} used to connect with
      * {@link MediaStore#AUTHORITY}. Typically this value is {@link Context#getContentResolver()}.
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 728983b..318fbed 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -19,7 +19,7 @@
     <string name="uid_label" msgid="8421971615411294156">"מדיה"</string>
     <string name="storage_description" msgid="4081716890357580107">"אחסון מקומי"</string>
     <string name="app_label" msgid="9035307001052716210">"אחסון מדיה"</string>
-    <string name="artist_label" msgid="8105600993099120273">"אמן"</string>
+    <string name="artist_label" msgid="8105600993099120273">"אומן"</string>
     <string name="unknown" msgid="2059049215682829375">"לא ידוע"</string>
     <string name="root_images" msgid="5861633549189045666">"תמונות"</string>
     <string name="root_videos" msgid="8792703517064649453">"סרטונים"</string>
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 96d863c..416fee7 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -27,6 +27,7 @@
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.database.Cursor.FIELD_TYPE_BLOB;
 import static android.provider.MediaStore.MATCH_DEFAULT;
 import static android.provider.MediaStore.MATCH_EXCLUDE;
 import static android.provider.MediaStore.MATCH_INCLUDE;
@@ -153,6 +154,7 @@
 import android.provider.BaseColumns;
 import android.provider.Column;
 import android.provider.DeviceConfig;
+import android.provider.DocumentsContract;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Audio.AudioColumns;
@@ -262,7 +264,10 @@
             "(?:image_id|video_id)\\s*=\\s*(\\d+)");
 
     /** File access by uid requires the transcoding transform */
-    private static final int FLAG_TRANSFORM_TRANSCODING = 1;
+    private static final int FLAG_TRANSFORM_TRANSCODING = 1 << 0;
+
+    /** File access by uid is a synthetic path corresponding to a redacted URI */
+    private static final int FLAG_TRANSFORM_REDACTION = 1 << 1;
 
     /**
      * These directory names aren't declared in Environment as final variables, and so we need to
@@ -289,6 +294,9 @@
     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";
+    public static final int REDACTED_URI_ID_SIZE = 36;
 
     /**
      * Hard-coded filename where the current value of
@@ -379,7 +387,9 @@
 
     private static final int sUserId = UserHandle.myUserId();
 
-    // WARNING/TODO (b/173505864): This will be replaced by signature APIs in S
+    /**
+     * Please use {@link getDownloadsProviderAuthority()} instead of using this directly.
+     */
     private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
 
     @GuardedBy("mPendingOpenInfo")
@@ -997,6 +1007,9 @@
 
         mTranscodeHelper = new TranscodeHelper(context, this);
 
+        // Create dir for redacted URI's path.
+        new File(getStorageRootPathForUid(UserHandle.myUserId()), REDACTED_URI_DIR).mkdirs();
+
         final IntentFilter packageFilter = new IntentFilter();
         packageFilter.setPriority(10);
         packageFilter.addDataScheme("package");
@@ -1054,15 +1067,14 @@
         }
 
         ProviderInfo provider = mPackageManager.resolveContentProvider(
-            DOWNLOADS_PROVIDER_AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
+                getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE
                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
         if (provider != null) {
             mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
         }
 
-        provider = mPackageManager.resolveContentProvider(
-            MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
-                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+        provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(),
+                PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
         if (provider != null) {
             mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
         }
@@ -1491,6 +1503,10 @@
     @Keep
     public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) {
         uid = getBinderUidForFuse(uid, tid);
+        if (isSyntheticFilePathForRedactedUri(path, uid)) {
+            return getFileLookupResultsForRedactedUriPath(uid, path);
+        }
+
         String ioPath = "";
         boolean transformsComplete = true;
         boolean transformsSupported = mTranscodeHelper.supportsTranscode(path);
@@ -1520,6 +1536,50 @@
                 transformsSupported, ioPath);
     }
 
+    private boolean isSyntheticFilePathForRedactedUri(String path, int uid) {
+        if (path == null) return false;
+
+        final String transformsSyntheticDir = getStorageRootPathForUid(uid) + "/"
+                + REDACTED_URI_DIR;
+        final String fileName = extractDisplayName(path);
+        return fileName != null && path.toLowerCase(Locale.ROOT).startsWith(
+                transformsSyntheticDir.toLowerCase(Locale.ROOT)) && fileName.startsWith(
+                REDACTED_URI_ID_PREFIX) && fileName.length() == REDACTED_URI_ID_SIZE;
+    }
+
+    private boolean isSyntheticDirPath(String path, int uid) {
+        final String transformsSyntheticDir = getStorageRootPathForUid(uid) + "/"
+                + TRANSFORMS_SYNTHETIC_DIR;
+        return path != null && path.toLowerCase(Locale.ROOT).startsWith(
+                transformsSyntheticDir.toLowerCase(Locale.ROOT));
+    }
+
+    private FileLookupResult getFileLookupResultsForRedactedUriPath(int uid, @NonNull String path) {
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        final String fileName = extractDisplayName(path);
+
+        final DatabaseHelper helper;
+        try {
+            helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
+        } catch (VolumeNotFoundException e) {
+            throw new IllegalStateException("Volume not found for file: " + path);
+        }
+
+        try (final Cursor c = helper.runWithoutTransaction(
+                (db) -> db.query("files", new String[]{MediaColumns.DATA},
+                        FileColumns.REDACTED_URI_ID + "=?", new String[]{fileName}, null, null,
+                        null))) {
+            if (!c.moveToFirst()) {
+                return new FileLookupResult(FLAG_TRANSFORM_REDACTION, 0, uid, false, true, null);
+            }
+
+            return new FileLookupResult(FLAG_TRANSFORM_REDACTION, 0, uid, true, true,
+                    c.getString(0));
+        } finally {
+            restoreLocalCallingIdentity(token);
+        }
+    }
+
     public int getBinderUidForFuse(int uid, int tid) {
         if (uid != MY_UID) {
             return uid;
@@ -2572,6 +2632,15 @@
         final LocalCallingIdentity token = clearLocalCallingIdentity(
                 LocalCallingIdentity.fromExternal(getContext(), uid));
 
+        if(isRedactedUri(uri)) {
+            if((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+                // we don't allow write grants on redacted uris.
+                return PackageManager.PERMISSION_DENIED;
+            }
+
+            uri = getUriForRedactedUri(uri);
+        }
+
         try {
             final boolean allowHidden = isCallingPackageAllowedHidden();
             final int table = matchUri(uri, allowHidden);
@@ -2658,6 +2727,12 @@
         final ArraySet<String> honoredArgs = new ArraySet<>();
         DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
 
+        Uri redactedUri = null;
+        if (isRedactedUri(uri)) {
+            redactedUri = uri;
+            uri = getUriForRedactedUri(uri);
+        }
+
         uri = safeUncanonicalize(uri);
 
         final String volumeName = getVolumeName(uri);
@@ -2770,6 +2845,16 @@
                     honoredArgs.toArray(new String[honoredArgs.size()]));
             c.setExtras(extras);
         }
+
+        // Query was on a redacted URI, update the sensitive information such as the _ID, DATA etc.
+        if (redactedUri != null && c != null) {
+            try {
+                return getRedactedUriCursor(redactedUri, c);
+            } finally {
+                c.close();
+            }
+        }
+
         return c;
     }
 
@@ -2778,6 +2863,95 @@
         return REDACTED_URI_SUPPORTED_TYPES.contains(match);
     }
 
+    private Cursor getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c) {
+        final HashSet<String> columnNames = new HashSet<>(Arrays.asList(c.getColumnNames()));
+        final MatrixCursor redactedUriCursor = new MatrixCursor(c.getColumnNames());
+        final String redactedUriId = redactedUri.getLastPathSegment();
+
+        if (!c.moveToFirst()) {
+            return redactedUriCursor;
+        }
+
+        // NOTE: It is safe to assume that there will only be one entry corresponding to a
+        // redacted URI as it corresponds to a unique DB entry.
+        if (c.getCount() != 1) {
+            throw new AssertionError("Two rows corresponding to " + redactedUri.toString()
+                    + " found, when only one expected");
+        }
+
+        final MatrixCursor.RowBuilder row = redactedUriCursor.newRow();
+        for (String columnName : c.getColumnNames()) {
+            final int colIndex = c.getColumnIndex(columnName);
+            if (c.getType(colIndex) == FIELD_TYPE_BLOB) {
+                row.add(c.getBlob(colIndex));
+            } else {
+                row.add(c.getString(colIndex));
+            }
+        }
+
+        updateRow(columnNames, MediaColumns._ID, row, redactedUriId);
+        updateRow(columnNames, MediaColumns.DISPLAY_NAME, row, redactedUriId);
+        updateRow(columnNames, MediaColumns.RELATIVE_PATH, row, REDACTED_URI_DIR);
+        updateRow(columnNames, MediaColumns.BUCKET_DISPLAY_NAME, row, REDACTED_URI_DIR);
+        updateRow(columnNames, MediaColumns.DATA, row, getPathForRedactedUriId(redactedUriId));
+        updateRow(columnNames, MediaColumns.DOCUMENT_ID, row, null);
+        updateRow(columnNames, MediaColumns.INSTANCE_ID, row, null);
+        updateRow(columnNames, MediaColumns.BUCKET_ID, row, null);
+
+        return redactedUriCursor;
+    }
+
+    static private String getPathForRedactedUriId(String redactedUriId) {
+        return getStorageRootPathForUid(Binder.getCallingUid()) + "/" + REDACTED_URI_DIR + "/"
+                + redactedUriId;
+    }
+
+    static private String getStorageRootPathForUid(int uid) {
+        return "/storage/emulated/" + (uid / PER_USER_RANGE);
+    }
+
+    private void updateRow(HashSet<String> columnNames, String columnName,
+            MatrixCursor.RowBuilder row, Object val) {
+        if (columnNames.contains(columnName)) {
+            row.add(columnName, val);
+        }
+    }
+
+    private Uri getUriForRedactedUri(Uri redactedUri) {
+        final Uri.Builder builder = redactedUri.buildUpon();
+        builder.path(null);
+        final List<String> segments = redactedUri.getPathSegments();
+        for (int i = 0; i < segments.size() - 1; i++) {
+            builder.appendPath(segments.get(i));
+        }
+
+        DatabaseHelper helper;
+        try {
+            helper = getDatabaseForUri(redactedUri);
+        } catch (VolumeNotFoundException e) {
+            throw e.rethrowAsIllegalArgumentException();
+        }
+
+        try (final Cursor c = helper.runWithoutTransaction(
+                (db) -> db.query("files", new String[]{MediaColumns._ID},
+                        FileColumns.REDACTED_URI_ID + "=?",
+                        new String[]{redactedUri.getLastPathSegment()}, null, null, null))) {
+            if (!c.moveToFirst()) {
+                throw new IllegalArgumentException(
+                        "Uri: " + redactedUri.toString() + " not found.");
+            }
+
+            builder.appendPath(c.getString(0));
+            return builder.build();
+        }
+    }
+
+    private boolean isRedactedUri(Uri uri) {
+        String id = uri.getLastPathSegment();
+        return id != null && id.startsWith(REDACTED_URI_ID_PREFIX)
+                && id.length() == REDACTED_URI_ID_SIZE;
+    }
+
     @Override
     public String getType(Uri url) {
         final int match = matchUri(url, true);
@@ -3680,6 +3854,7 @@
         }
 
         final long rowId;
+        Uri newUri = uri;
         {
             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
                 String name = values.getAsString(Audio.Playlists.NAME);
@@ -3706,6 +3881,9 @@
                         values.put(FileColumns.SIZE, file.length());
                     }
                 }
+                if (!isFuseThread() && shouldFileBeHidden(file)) {
+                    newUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri));
+                }
             }
 
             rowId = insertAllowingUpsert(qb, helper, values, path);
@@ -3716,7 +3894,7 @@
             }
         }
 
-        return ContentUris.withAppendedId(uri, rowId);
+        return ContentUris.withAppendedId(newUri, rowId);
     }
 
     /**
@@ -4961,6 +5139,11 @@
             throws FallbackException {
         extras = (extras != null) ? extras : new Bundle();
 
+        if (isRedactedUri(uri)) {
+            // we don't support deletion on redacted uris.
+            return 0;
+        }
+
         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
 
@@ -4968,7 +5151,7 @@
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int match = matchUri(uri, allowHidden);
 
-        switch(match) {
+        switch (match) {
             case AUDIO_MEDIA_ID:
             case AUDIO_PLAYLISTS_ID:
             case VIDEO_MEDIA_ID:
@@ -5063,6 +5246,7 @@
             };
             final boolean isFilesTable = qb.getTables().equals("files");
             final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
+            final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
             if (isFilesTable) {
                 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
                 if (deleteparam == null || ! deleteparam.equals("false")) {
@@ -5080,7 +5264,13 @@
                             mCallingIdentity.get().setOwned(id, false);
 
                             deleteIfAllowed(uri, extras, data);
-                            count += qb.delete(helper, BaseColumns._ID + "=" + id, null);
+                            int res = qb.delete(helper, BaseColumns._ID + "=" + id, null);
+                            count += res;
+                            // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
+                            // but mediaTypeSize is not updated
+                            if (res > 0 && mediaType < countPerMediaType.length) {
+                                countPerMediaType[mediaType] += res;
+                            }
 
                             if (isDownload == 1) {
                                 deletedDownloadIds.put(id, mimeType);
@@ -5136,7 +5326,7 @@
 
             if (isFilesTable && !isCallingPackageSelf()) {
                 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
-                        getCallingPackageOrSelf(), count);
+                        getCallingPackageOrSelf(), count, countPerMediaType);
             }
         }
 
@@ -5393,7 +5583,7 @@
 
                 try (ContentProviderClient client = getContext().getContentResolver()
                         .acquireUnstableContentProviderClient(
-                                MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+                                getExternalStorageProviderAuthority())) {
                     extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
                     return client.call(method, null, extras);
                 } catch (RemoteException e) {
@@ -5408,7 +5598,7 @@
                 final Uri fileUri;
                 try (ContentProviderClient client = getContext().getContentResolver()
                         .acquireUnstableContentProviderClient(
-                                MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+                                getExternalStorageProviderAuthority())) {
                     final Bundle res = client.call(method, null, extras);
                     fileUri = res.getParcelable(MediaStore.EXTRA_URI);
                 } catch (RemoteException e) {
@@ -5470,8 +5660,12 @@
                         extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR);
                 try {
                     File file = getFileFromFileDescriptor(inputPfd);
-                    FuseDaemon fuseDaemon = getFuseDaemonForFile(file);
+                    boolean supportsTranscode = mTranscodeHelper.supportsTranscode(file.getPath());
+                    if (!supportsTranscode) {
+                        throw new IOException("Input file descriptor is already original");
+                    }
 
+                    FuseDaemon fuseDaemon = getFuseDaemonForFile(file);
                     String outputPath = fuseDaemon.getOriginalMediaFormatFilePath(inputPfd);
                     if (TextUtils.isEmpty(outputPath)) {
                         throw new IOException("Invalid path for original media format file");
@@ -5892,6 +6086,11 @@
             @Nullable Bundle extras) throws FallbackException {
         extras = (extras != null) ? extras : new Bundle();
 
+        if (isRedactedUri(uri)) {
+            // we don't support update on redacted uris.
+            return 0;
+        }
+
         // 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);
@@ -6790,6 +6989,11 @@
     private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal,
             @Nullable Bundle opts)
             throws FileNotFoundException {
+        boolean isRedactedUri = false;
+        if (isRedactedUri(uri)) {
+            uri = getUriForRedactedUri(uri);
+            isRedactedUri = true;
+        }
         uri = safeUncanonicalize(uri);
         opts = opts == null ? new Bundle() : opts;
 
@@ -6825,7 +7029,8 @@
             }
         }
 
-        return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts);
+        return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts,
+                isRedactedUri);
     }
 
     @Override
@@ -7138,11 +7343,14 @@
      * a "/mnt/user" path.
      */
     private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match,
-            String mode, CancellationSignal signal, @NonNull Bundle opts)
+            String mode, CancellationSignal signal, @NonNull Bundle opts, boolean isRedactedUri)
             throws FileNotFoundException {
         int modeBits = ParcelFileDescriptor.parseMode(mode);
         boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0;
         if (forWrite) {
+            if (isRedactedUri) {
+                throw new UnsupportedOperationException("Write is not supported on redacted URIs");
+            }
             // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling
             // #shouldOpenWithFuse
             modeBits |= ParcelFileDescriptor.MODE_READ_WRITE;
@@ -7183,7 +7391,7 @@
 
         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
         // Figure out if we need to redact contents
-        final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri);
+        final boolean redactionNeeded = isRedactedUri || (!callerIsOwner && isRedactionNeeded(uri));
         final RedactionInfo redactionInfo;
         try {
             redactionInfo = redactionNeeded ? getRedactionRanges(file)
@@ -7571,13 +7779,17 @@
      */
     @NonNull
     private long[] getRedactionRangesForFuse(String path, String ioPath, int original_uid, int uid,
-            int tid) throws IOException {
+            int tid, boolean forceRedaction) throws IOException {
         // |ioPath| might refer to a transcoded file path (which is not indexed in the db)
         // |path| will always refer to a valid _data column
         // We use |ioPath| for the filesystem access because in the case of transcoding,
         // we want to get redaction ranges from the transcoded file and *not* the original file
         final File file = new File(ioPath);
 
+        if (forceRedaction) {
+            return getRedactionRanges(file).redactionRanges;
+        }
+
         // When calculating redaction ranges initiated from MediaProvider, the redaction policy
         // is slightly different from the FUSE initiated opens redaction policy. targetSdk=29 from
         // MediaProvider requires redaction, but targetSdk=29 apps from FUSE don't require redaction
@@ -7608,7 +7820,7 @@
             final String[] projection = new String[]{
                     MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID };
             final String selection = MediaColumns.DATA + "=?";
-            final String[] selectionArgs = new String[] { path };
+            final String[] selectionArgs = new String[]{path};
             final String ownerPackageName;
             final Uri item;
             try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
@@ -7627,6 +7839,7 @@
 
             final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(),
                     ownerPackageName);
+
             if (callerIsOwner) {
                 return new long[0];
             }
@@ -7741,6 +7954,27 @@
         }
 
         try {
+            boolean forceRedaction = false;
+            if (isSyntheticFilePathForRedactedUri(path, uid)) {
+                if (forWrite) {
+                    // Redacted URIs are not allowed to update EXIF headers.
+                    return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
+                            mediaCapabilitiesUid, new long[0]);
+                }
+
+                // If path is redacted Uris' path, ioPath must be the real path, ioPath must
+                // haven been updated to the real path during onFileLookupForFuse.
+                path = ioPath;
+
+                // Irrespective of the permissions we want to redact in this case.
+                redact = true;
+                forceRedaction = true;
+            } else if (isSyntheticDirPath(path, uid)) {
+                // we don't support any other transformations under .transforms/synthetic dir
+                return new FileOpenResult(OsConstants.ENOENT /* status */, originalUid,
+                        mediaCapabilitiesUid, new long[0]);
+            }
+
             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
                 Log.e(TAG, "Can't open a file in another app's external directory!");
                 return new FileOpenResult(OsConstants.ENOENT, originalUid, mediaCapabilitiesUid,
@@ -7750,8 +7984,8 @@
             if (shouldBypassFuseRestrictions(forWrite, path)) {
                 isSuccess = true;
                 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
-                        redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid) :
-                                new long[0]);
+                        redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
+                                forceRedaction) : new long[0]);
             }
             // Legacy apps that made is this far don't have the right storage permission and hence
             // are not allowed to access anything other than their external app directory
@@ -7805,8 +8039,8 @@
             }
             isSuccess = true;
             return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
-                    redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid) :
-                            new long[0]);
+                    redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
+                            forceRedaction) : new long[0]);
         } catch (IOException e) {
             // We are here because
             // * There is no db row corresponding to the requested path, which is more unlikely.
@@ -8371,6 +8605,30 @@
         return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID);
     }
 
+    private String getExternalStorageProviderAuthority() {
+        if (SdkLevel.isAtLeastS()) {
+            return getExternalStorageProviderAuthorityFromDocumentsContract();
+        }
+        return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    private String getExternalStorageProviderAuthorityFromDocumentsContract() {
+        return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
+    }
+
+    private String getDownloadsProviderAuthority() {
+        if (SdkLevel.isAtLeastS()) {
+            return getDownloadsProviderAuthorityFromDocumentsContract();
+        }
+        return DOWNLOADS_PROVIDER_AUTHORITY;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    private String getDownloadsProviderAuthorityFromDocumentsContract() {
+        return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
+    }
+
     private boolean isCallingIdentityDownloadProvider(int uid) {
         return uid == mDownloadsAuthorityAppId;
     }
@@ -8387,9 +8645,9 @@
      * The following apps have access to all private-app directories on secondary volumes:
      *    * ExternalStorageProvider
      *    * DownloadProvider
-     *    * Signature/privileged apps with ACCESS_MTP permission granted
-     *      (TODO(b/175796984): Allow *only* signature apps with ACCESS_MTP to access all
-     *      private-app directories).
+     *    * Signature apps with ACCESS_MTP permission granted
+     *      (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all
+     *      private-app directories, this additional access is removed for Android S+).
      *
      * Installer apps can only access private-app directories on Android/obb.
      *
@@ -8400,16 +8658,43 @@
         final LocalCallingIdentity token =
             clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
         try {
-            if (isCallingIdentityDownloadProvider(uid) ||
-                    isCallingIdentityExternalStorageProvider(uid) || isCallingIdentityMtp(uid)) {
-                return true;
+            if (SdkLevel.isAtLeastS()) {
+                return isMountModeAllowedPrivatePathAccess(uid, getCallingPackage(), path);
+            } else {
+                if (isCallingIdentityDownloadProvider(uid) ||
+                        isCallingIdentityExternalStorageProvider(uid) || isCallingIdentityMtp(
+                        uid)) {
+                    return true;
+                }
+                return (isObbOrChildPath(path) && isCallingIdentityAllowedInstallerAccess(uid));
             }
-            return (isObbOrChildPath(path) && isCallingIdentityAllowedInstallerAccess(uid));
         } finally {
             restoreLocalCallingIdentity(token);
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.S)
+    private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName, String path) {
+        // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access
+        // mount modes.
+        final CallingIdentity token = clearCallingIdentity();
+        try {
+            final int mountMode = mStorageManager.getExternalStorageMountMode(uid, packageName);
+            switch (mountMode) {
+                case StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE:
+                case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH:
+                    return true;
+                case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER:
+                    return isObbOrChildPath(path);
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e);
+        } finally {
+            restoreCallingIdentity(token);
+        }
+        return false;
+    }
+
     private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
         // System internals can work with all media
         if (isCallingPackageSelf() || isCallingPackageShell()) {
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index f3598c8..ee37448 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -22,6 +22,10 @@
 import static com.android.providers.media.MediaProvider.collectUris;
 import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
 import static com.android.providers.media.util.Logging.TAG;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -64,6 +68,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.providers.media.MediaProvider.LocalUriMatcher;
 import com.android.providers.media.util.Metrics;
@@ -105,14 +110,26 @@
     private AsyncTask<Void, Void, Void> positiveActionTask;
     private Dialog progressDialog;
     private TextView titleView;
+    private Handler mHandler;
+    private Runnable mShowProgressDialogRunnable = () -> {
+        // We will show the progress dialog, add the dim effect back.
+        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+        progressDialog.show();
+    };
 
     private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L;
+    private static final Long BEFORE_SHOW_PROGRESS_TIME_MS = 300L;
 
-    private static final String VERB_WRITE = "write";
-    private static final String VERB_TRASH = "trash";
+    @VisibleForTesting
+    static final String VERB_WRITE = "write";
+    @VisibleForTesting
+    static final String VERB_TRASH = "trash";
+    @VisibleForTesting
+    static final String VERB_FAVORITE = "favorite";
+    @VisibleForTesting
+    static final String VERB_UNFAVORITE = "unfavorite";
+
     private static final String VERB_UNTRASH = "untrash";
-    private static final String VERB_FAVORITE = "favorite";
-    private static final String VERB_UNFAVORITE = "unfavorite";
     private static final String VERB_DELETE = "delete";
 
     private static final String DATA_AUDIO = "audio";
@@ -136,6 +153,12 @@
         getWindow().addSystemFlags(
                 WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
         setFinishOnTouchOutside(false);
+        // remove the dim effect
+        // We may not show the progress dialog, if we don't remove the dim effect,
+        // it may have flicker.
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+        getWindow().setDimAmount(0.0f);
+
 
         // All untrusted input values here were validated when generating the
         // original PendingIntent
@@ -154,19 +177,16 @@
             return;
         }
 
+        mHandler = new Handler(getMainLooper());
         // Create Progress dialog
         createProgressDialog();
 
-        // Favorite-related requests are automatically granted for now; we still
-        // make developers go through this no-op dialog flow to preserve our
-        // ability to start prompting in the future
-        switch (verb) {
-            case VERB_FAVORITE:
-            case VERB_UNFAVORITE: {
-                onPositiveAction(null, 0);
-                return;
-            }
+        if (!shouldShowActionDialog(this, -1 /* pid */, appInfo.uid, getCallingPackage(),
+                null /* attributionTag */, verb)) {
+            onPositiveAction(null, 0);
+            return;
         }
+
         // Kick off async loading of description to show in dialog
         final View bodyView = getLayoutInflater().inflate(R.layout.permission_body, null, false);
         handleImageViewVisibility(bodyView, uris);
@@ -220,6 +240,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
+        mHandler.removeCallbacks(mShowProgressDialogRunnable);
         // Cancel and interrupt the AsyncTask of the positive action. This avoids
         // calling the old activity during "onPostExecute", but the AsyncTask could
         // still finish its background task. For now we are ok with:
@@ -245,8 +266,10 @@
             ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
         }
 
-        progressDialog.show();
         final long startTime = System.currentTimeMillis();
+
+        mHandler.postDelayed(mShowProgressDialogRunnable, BEFORE_SHOW_PROGRESS_TIME_MS);
+
         positiveActionTask = new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... params) {
@@ -297,17 +320,23 @@
             @Override
             protected void onPostExecute(Void result) {
                 setResult(Activity.RESULT_OK);
-                // Don't dismiss the progress dialog too quick, it will cause bad UX.
-                final long duration = System.currentTimeMillis() - startTime;
-                if (duration > LEAST_SHOW_PROGRESS_TIME_MS) {
-                    progressDialog.dismiss();
+                mHandler.removeCallbacks(mShowProgressDialogRunnable);
+
+                if (!progressDialog.isShowing()) {
                     finish();
                 } else {
-                    Handler handler = new Handler(getMainLooper());
-                    handler.postDelayed(() -> {
+                    // Don't dismiss the progress dialog too quick, it will cause bad UX.
+                    final long duration =
+                            System.currentTimeMillis() - startTime - BEFORE_SHOW_PROGRESS_TIME_MS;
+                    if (duration > LEAST_SHOW_PROGRESS_TIME_MS) {
                         progressDialog.dismiss();
                         finish();
-                    }, LEAST_SHOW_PROGRESS_TIME_MS - duration);
+                    } else {
+                        mHandler.postDelayed(() -> {
+                            progressDialog.dismiss();
+                            finish();
+                        }, LEAST_SHOW_PROGRESS_TIME_MS - duration);
+                    }
                 }
             }
         }.execute();
@@ -343,6 +372,37 @@
         return keyCode == KeyEvent.KEYCODE_BACK;
     }
 
+    @VisibleForTesting
+    static boolean shouldShowActionDialog(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag, @NonNull String verb) {
+        // Favorite-related requests are automatically granted for now; we still
+        // make developers go through this no-op dialog flow to preserve our
+        // ability to start prompting in the future
+        if (TextUtils.equals(VERB_FAVORITE, verb) || TextUtils.equals(VERB_UNFAVORITE, verb)) {
+            return false;
+        }
+
+        // check READ_EXTERNAL_STORAGE and MANAGE_EXTERNAL_STORAGE permissions
+        if (!checkPermissionReadStorage(context, pid, uid, packageName, attributionTag)
+                && !checkPermissionManager(context, pid, uid, packageName, attributionTag)) {
+            Log.d(TAG, "No permission READ_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE");
+            return true;
+        }
+        // check MANAGE_MEDIA permission
+        if (!checkPermissionManageMedia(context, pid, uid, packageName, attributionTag)) {
+            Log.d(TAG, "No permission MANAGE_MEDIA");
+            return true;
+        }
+
+        // if verb is write, check ACCESS_MEDIA_LOCATION permission
+        if (TextUtils.equals(verb, VERB_WRITE) && !checkPermissionAccessMediaLocation(context, pid,
+                uid, packageName, attributionTag)) {
+            Log.d(TAG, "No permission ACCESS_MEDIA_LOCATION");
+            return true;
+        }
+        return false;
+    }
+
     private void handleImageViewVisibility(View bodyView, List<Uri> uris) {
         if (uris.isEmpty()) {
             return;
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index 9fcae72..b19d321 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -279,8 +279,19 @@
     }
 
     public void freeCache(long bytes) {
-        // TODO(b/181846007): Implement cache clearing policies.
-        mTranscodeDirectory.delete();
+        File[] files = mTranscodeDirectory.listFiles();
+        for (File file : files) {
+            if (bytes <= 0) {
+                return;
+            }
+            if (file.exists() && file.isFile()) {
+                long size = file.length();
+                boolean deleted = file.delete();
+                if (deleted) {
+                    bytes -= size;
+                }
+            }
+        }
     }
 
     private UUID getTranscodeVolumeUuid() {
@@ -438,9 +449,9 @@
 
             failureReason = waitTranscodingResult(uid, src, transcodingSession, latch);
             errorCode = transcodingSession.getErrorCode();
-            boolean success = failureReason == TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN;
+            result = failureReason == TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN;
 
-            if (success) {
+            if (result) {
                 updateTranscodeStatus(src, TRANSCODE_COMPLETE);
             } else {
                 logEvent("Transcoding failed for " + src + ". session: ", transcodingSession);
@@ -1451,7 +1462,6 @@
     private static class StorageTranscodingSession {
         public final TranscodingSession session;
         public final CountDownLatch latch;
-        private final Set<Integer> mBlockedUids = new ArraySet<>();
         private boolean hasAnr;
 
         public StorageTranscodingSession(TranscodingSession session, CountDownLatch latch) {
@@ -1460,15 +1470,11 @@
         }
 
         public void addBlockedUid(int uid) {
-            synchronized (latch) {
-                mBlockedUids.add(uid);
-            }
+            session.addClientUid(uid);
         }
 
         public boolean isUidBlocked(int uid) {
-            synchronized (latch) {
-                return mBlockedUids.contains(uid);
-            }
+            return session.getClientUids().contains(uid);
         }
 
         public void setAnr() {
@@ -1485,7 +1491,7 @@
 
         @Override
         public String toString() {
-            return session.toString() + ". BlockedUids: " + mBlockedUids;
+            return session.toString() + ". BlockedUids: " + session.getClientUids();
         }
     }
 
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index c88fc20..72c313c 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -48,6 +48,8 @@
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
+import static com.android.providers.media.util.Metrics.translateReason;
+
 import android.content.ContentProviderClient;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
@@ -514,12 +516,20 @@
             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
 
-            try (Cursor c = mResolver.query(mFilesUri, new String[] { FileColumns._ID },
+            final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
+            try (Cursor c = mResolver.query(mFilesUri,
+                    new String[] { FileColumns._ID, FileColumns.MEDIA_TYPE },
                     queryArgs, mSignal)) {
                 while (c.moveToNext()) {
                     final long id = c.getLong(0);
                     if (Arrays.binarySearch(scannedIds, id) < 0) {
                         mUnknownIds.add(id);
+                        final int mediaType = c.getInt(1);
+                        // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
+                        // but mediaTypeSize is not updated
+                        if (mediaType < countPerMediaType.length) {
+                            countPerMediaType[mediaType]++;
+                        }
                     }
                 }
             } finally {
@@ -541,6 +551,10 @@
                 }
                 applyPending();
             } finally {
+                if (mUnknownIds.size() > 0) {
+                    String scanReason = "scan triggered by reason: " + translateReason(mReason);
+                    Metrics.logDeletionPersistent(mVolumeName, scanReason, countPerMediaType);
+                }
                 Trace.endSection();
             }
         }
diff --git a/src/com/android/providers/media/util/Metrics.java b/src/com/android/providers/media/util/Metrics.java
index 86a0302..410da3a 100644
--- a/src/com/android/providers/media/util/Metrics.java
+++ b/src/com/android/providers/media/util/Metrics.java
@@ -60,12 +60,30 @@
                 normalizedInsertCount, normalizedUpdateCount, normalizedDeleteCount);
     }
 
-    public static void logDeletion(@NonNull String volumeName, int uid, String packageName,
-            int itemCount) {
-        Logging.logPersistent(String.format(
-                "Deleted %3$d items on %1$s due to %2$s",
-                volumeName, packageName, itemCount));
+    /**
+     * Logs persistent deletion logs on-device.
+     */
+    public static void logDeletionPersistent(@NonNull String volumeName, String reason,
+            int[] countPerMediaType) {
+        final StringBuilder builder = new StringBuilder("Deleted ");
+        for (int count: countPerMediaType) {
+            builder.append(count).append(' ');
+        }
+        builder.append("items on ")
+                .append(volumeName)
+                .append(" due to ")
+                .append(reason);
 
+        Logging.logPersistent(builder.toString());
+    }
+
+    /**
+     * Logs persistent deletion logs on-device and stats metrics. Count of items per-media-type
+     * are not uploaded to MediaProviderStats logs.
+     */
+    public static void logDeletion(@NonNull String volumeName, int uid, String packageName,
+            int itemCount, int[] countPerMediaType) {
+        logDeletionPersistent(volumeName, packageName, countPerMediaType);
         MediaProviderStatsLog.write(MEDIA_CONTENT_DELETED,
                 translateVolumeName(volumeName), uid, itemCount);
     }
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index d9765ac..5b3639c 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -16,10 +16,12 @@
 
 package com.android.providers.media.util;
 
+import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
 import static android.Manifest.permission.ACCESS_MTP;
 import static android.Manifest.permission.BACKUP;
 import static android.Manifest.permission.INSTALL_PACKAGES;
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
+import static android.Manifest.permission.MANAGE_MEDIA;
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
@@ -114,6 +116,26 @@
                 generateAppOpMessage(packageName,sOpDescription.get()));
     }
 
+    /**
+     * Check if the given package has been granted the
+     * android.Manifest.permission#ACCESS_MEDIA_LOCATION permission.
+     */
+    public static boolean checkPermissionAccessMediaLocation(@NonNull Context context, int pid,
+            int uid, @NonNull String packageName, @Nullable String attributionTag) {
+        return checkPermissionForDataDelivery(context, ACCESS_MEDIA_LOCATION, pid, uid, packageName,
+                attributionTag, generateAppOpMessage(packageName, sOpDescription.get()));
+    }
+
+    /**
+     * Check if the given package has been granted the
+     * android.Manifest.permission#MANAGE_MEDIA permission.
+     */
+    public static boolean checkPermissionManageMedia(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag) {
+        return checkPermissionForDataDelivery(context, MANAGE_MEDIA, pid, uid, packageName,
+                attributionTag, generateAppOpMessage(packageName, sOpDescription.get()));
+    }
+
     public static boolean checkIsLegacyStorageGranted(@NonNull Context context, int uid,
             String packageName, @Nullable String attributionTag) {
         if (context.getSystemService(AppOpsManager.class)
@@ -407,6 +429,7 @@
     private static boolean isAppOpPermission(String permission) {
         switch (permission) {
             case MANAGE_EXTERNAL_STORAGE:
+            case MANAGE_MEDIA:
                 return true;
         }
         return false;
@@ -414,6 +437,7 @@
 
     private static boolean isRuntimePermission(String permission) {
         switch (permission) {
+            case ACCESS_MEDIA_LOCATION:
             case READ_EXTERNAL_STORAGE:
             case WRITE_EXTERNAL_STORAGE:
                 return true;
diff --git a/tests/client/AndroidTest.xml b/tests/client/AndroidTest.xml
index b9c3dfa..22ca7d3 100644
--- a/tests/client/AndroidTest.xml
+++ b/tests/client/AndroidTest.xml
@@ -27,8 +27,4 @@
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="hidden-api-checks" value="false"/>
     </test>
-
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
-    </object>
 </configuration>
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index 7bbce62..ce72d97 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -16,6 +16,26 @@
 
 package com.android.providers.media;
 
+import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
+import static android.Manifest.permission.MANAGE_APP_OPS_MODES;
+import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
+import static android.Manifest.permission.MANAGE_MEDIA;
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.android.providers.media.PermissionActivity.VERB_FAVORITE;
+import static com.android.providers.media.PermissionActivity.VERB_TRASH;
+import static com.android.providers.media.PermissionActivity.VERB_UNFAVORITE;
+import static com.android.providers.media.PermissionActivity.VERB_WRITE;
+import static com.android.providers.media.PermissionActivity.shouldShowActionDialog;
+import static com.android.providers.media.util.TestUtils.adoptShellPermission;
+import static com.android.providers.media.util.TestUtils.dropShellPermission;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AppOpsManager;
 import android.app.Instrumentation;
 import android.content.ClipData;
 import android.content.ContentValues;
@@ -30,6 +50,7 @@
 
 import com.android.providers.media.scan.MediaScannerTest;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -42,6 +63,17 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class PermissionActivityTest {
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.providers.media.testapp.withstorageperms";
+
+    private static final int TEST_APP_PID = -1;
+    private int mTestAppUid = -1;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestAppUid = getContext().getPackageManager().getPackageUid(TEST_APP_PACKAGE_NAME, 0);
+    }
+
     @Test
     public void testSimple() throws Exception {
         final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
@@ -52,6 +84,165 @@
         activity.startActivityForResult(createIntent(), 42);
     }
 
+    @Test
+    public void testShouldShowActionDialog_favorite_false() throws Exception {
+        assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                TEST_APP_PACKAGE_NAME, null, VERB_FAVORITE)).isFalse();
+    }
+
+    @Test
+    public void testShouldShowActionDialog_unfavorite_false() throws Exception {
+        assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                TEST_APP_PACKAGE_NAME, null, VERB_UNFAVORITE)).isFalse();
+    }
+
+    @Test
+    public void testShouldShowActionDialog_noRESAndMES_true() throws Exception {
+        final String[] enableAppOpsList = {AppOpsManager.permissionToOp(MANAGE_MEDIA)};
+        final String[] disableAppOpsList = {
+                AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+                AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            for (String op : enableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+            }
+
+            for (String op : disableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+            }
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                    TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testShouldShowActionDialog_noMANAGE_MEDIA_true() throws Exception {
+        final String[] enableAppOpsList = {
+                AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+                AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+        final String[] disableAppOpsList =  {AppOpsManager.permissionToOp(MANAGE_MEDIA)};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            for (String op : enableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+            }
+
+            for (String op : disableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+            }
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                    TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testShouldShowActionDialog_hasPermissionWithRES_false() throws Exception {
+        final String[] enableAppOpsList = {
+                AppOpsManager.permissionToOp(MANAGE_MEDIA),
+                AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+        final String[] disableAppOpsList = {AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE)};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            for (String op : enableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+            }
+
+            for (String op : disableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+            }
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                    TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testShouldShowActionDialog_hasPermissionWithMES_false() throws Exception {
+        final String[] enableAppOpsList = {
+                AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+                AppOpsManager.permissionToOp(MANAGE_MEDIA)};
+        final String[] disableAppOpsList = {AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            for (String op : enableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+            }
+
+            for (String op : disableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+            }
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                    TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true() throws Exception {
+        final String[] enableAppOpsList = {
+                AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+                AppOpsManager.permissionToOp(MANAGE_MEDIA),
+                AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+        final String[] disableAppOpsList = {AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION)};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            for (String op : enableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+            }
+
+            for (String op : disableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+            }
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                    TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isTrue();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testShouldShowActionDialog_writeHasACCESS_MEDIA_LOCATION_false() throws Exception {
+        final String[] enableAppOpsList = {
+                AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION),
+                AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+                AppOpsManager.permissionToOp(MANAGE_MEDIA),
+                AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+        final String[] disableAppOpsList = new String[]{};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            for (String op : enableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+            }
+
+            for (String op : disableAppOpsList) {
+                modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+            }
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+                    TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isFalse();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
     private static Intent createIntent() throws Exception {
         final Context context = InstrumentationRegistry.getContext();
 
@@ -67,4 +258,8 @@
         intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues());
         return intent;
     }
+
+    private static void modifyAppOp(int uid, String op, int mode) {
+        getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
+    }
 }
diff --git a/tests/src/com/android/providers/media/util/MetricsTest.java b/tests/src/com/android/providers/media/util/MetricsTest.java
index 41e25b5..688e2e9 100644
--- a/tests/src/com/android/providers/media/util/MetricsTest.java
+++ b/tests/src/com/android/providers/media/util/MetricsTest.java
@@ -41,7 +41,8 @@
         final String packageName = "com.example";
 
         Metrics.logScan(volumeName, MediaScanner.REASON_UNKNOWN, 42, 42, 42, 42, 42);
-        Metrics.logDeletion(volumeName, 42, packageName, 42);
+        Metrics.logDeletionPersistent(volumeName, "scanReason", new int[] { 42 });
+        Metrics.logDeletion(volumeName, 42, packageName, 42, new int[] { 42 });
         Metrics.logPermissionGranted(volumeName, 42, packageName, 42);
         Metrics.logPermissionDenied(volumeName, 42, packageName, 42);
         Metrics.logSchemaChange(volumeName, 42, 42, 42, 42);
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 1fc43cb..da89b5c 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -17,6 +17,8 @@
 package com.android.providers.media.util;
 
 import static android.Manifest.permission.MANAGE_APP_OPS_MODES;
+import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
+import static android.Manifest.permission.MANAGE_MEDIA;
 import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
 import static android.app.AppOpsManager.OPSTR_NO_ISOLATED_STORAGE;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
@@ -32,9 +34,11 @@
 import static com.android.providers.media.util.PermissionUtils.checkAppOpRequestInstallPackagesForSharedUid;
 import static com.android.providers.media.util.PermissionUtils.checkIsLegacyStorageGranted;
 import static com.android.providers.media.util.PermissionUtils.checkNoIsolatedStorageGranted;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMtp;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionInstallPackages;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
@@ -113,6 +117,9 @@
         assertThat(checkPermissionShell(context, pid, uid)).isFalse();
         assertThat(checkPermissionManager(context, pid, uid, packageName, null)).isFalse();
         assertThat(checkPermissionDelegator(context, pid, uid)).isFalse();
+        assertThat(checkPermissionManageMedia(context, pid, uid, packageName, null)).isFalse();
+        assertThat(checkPermissionAccessMediaLocation(context, pid, uid,
+                packageName, null)).isFalse();
 
         assertThat(checkPermissionReadStorage(context, pid, uid, packageName, null)).isTrue();
         assertThat(checkPermissionWriteStorage(context, pid, uid, packageName, null)).isTrue();
@@ -123,8 +130,7 @@
         assertThat(checkPermissionWriteVideo(context, pid, uid, packageName, null)).isFalse();
         assertThat(checkPermissionReadImages(context, pid, uid, packageName, null)).isTrue();
         assertThat(checkPermissionWriteImages(context, pid, uid, packageName, null)).isFalse();
-        assertThat(checkPermissionInstallPackages(getContext(), pid, uid,
-                getContext().getPackageName(), null)).isFalse();
+        assertThat(checkPermissionInstallPackages(context, pid, uid, packageName, null)).isFalse();
     }
 
     /**
@@ -149,9 +155,6 @@
             assertThat(checkPermissionSelf(getContext(), testAppPid, testAppUid)).isFalse();
             assertThat(checkPermissionShell(getContext(), testAppPid, testAppUid)).isFalse();
             assertThat(
-                    checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
-                            null)).isFalse();
-            assertThat(
                     checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
                             null)).isFalse();
             assertThat(
@@ -182,6 +185,13 @@
             assertThat(
                     checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
                             null)).isFalse();
+
+            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+                    null)).isFalse();
+
+            assertThat(checkPermissionAccessMediaLocation(getContext(), testAppPid, testAppUid,
+                    packageName, null)).isFalse();
+
             assertThat(
                     checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
                             null)).isFalse();
@@ -214,6 +224,13 @@
             assertThat(
                     checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
                             null)).isFalse();
+
+            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+                    null)).isFalse();
+
+            assertThat(checkPermissionAccessMediaLocation(getContext(), testAppPid, testAppUid,
+                    packageName, null)).isTrue();
+
             assertThat(
                     checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
                             null)).isTrue();
@@ -232,6 +249,51 @@
         }
     }
 
+    @Test
+    public void testManageExternalStoragePermissionsOnTestApp() throws Exception {
+        final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
+        final int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+        final int testAppPid = pidMap.get(packageName);
+        final String op = AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE);
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
+
+            assertThat(checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+                    null)).isFalse();
+
+            modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
+
+            assertThat(checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+                    null)).isTrue();
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testManageMediaPermissionsOnTestApp() throws Exception {
+        final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
+        final int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+        final int testAppPid = pidMap.get(packageName);
+        final String op = AppOpsManager.permissionToOp(MANAGE_MEDIA);
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
+
+            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+                    null)).isFalse();
+
+            modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
+
+            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+                    null)).isTrue();
+        } finally {
+            dropShellPermission();
+        }
+    }
 
     @Test
     public void testSystemGalleryPermissionsOnTestApp() throws Exception {
diff --git a/tests/src/com/android/providers/media/util/TestUtils.java b/tests/src/com/android/providers/media/util/TestUtils.java
index 1581b03..f2d3696 100644
--- a/tests/src/com/android/providers/media/util/TestUtils.java
+++ b/tests/src/com/android/providers/media/util/TestUtils.java
@@ -26,7 +26,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-class TestUtils {
+public class TestUtils {
     public static final String QUERY_TYPE = "com.android.providers.media.util.QUERY_TYPE";
     public static final String RUN_INFINITE_ACTIVITY =
             "com.android.providers.media.util.RUN_INFINITE_ACTIVITY";
diff --git a/tests/test_app/TestAppWithStoragePerms.xml b/tests/test_app/TestAppWithStoragePerms.xml
index 694e17f..c37b025 100644
--- a/tests/test_app/TestAppWithStoragePerms.xml
+++ b/tests/test_app/TestAppWithStoragePerms.xml
@@ -20,8 +20,11 @@
           android:versionCode="1"
           android:versionName="1.0">
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
     <application android:label="TestAppPerms">
         <activity android:name="com.android.providers.media.util.TestAppActivity"
diff --git a/tools/dialogs/AndroidManifest.xml b/tools/dialogs/AndroidManifest.xml
index 8ee5f2c..960cb13 100644
--- a/tools/dialogs/AndroidManifest.xml
+++ b/tools/dialogs/AndroidManifest.xml
@@ -5,6 +5,7 @@
 
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
 
     <application android:label="DialogsTool">
         <activity android:name=".DialogsActivity"