Merge "Skip scanning non-changed audio/mp4 files." into mainline-prod
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index fff9037..51f9e23 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -5062,11 +5062,14 @@
      * @return true if the given Files uri has media_type=MEDIA_TYPE_SUBTITLE
      */
     private boolean isSubtitleFile(Uri uri) {
+        final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
         try (Cursor cursor = queryForSingleItem(uri, new String[]{FileColumns.MEDIA_TYPE}, null,
                 null, null)) {
             return cursor.getInt(0) == FileColumns.MEDIA_TYPE_SUBTITLE;
         } catch (FileNotFoundException e) {
             Log.e(TAG, "Couldn't find database row for requested uri " + uri, e);
+        } finally {
+            restoreLocalCallingIdentity(tokenInner);
         }
         return false;
     }
@@ -6405,6 +6408,23 @@
     }
 
     /**
+     * Query the given {@link Uri} as MediaProvider, expecting only a single item to be found.
+     *
+     * @throws FileNotFoundException if no items were found, or multiple items
+     *             were found, or there was trouble reading the data.
+     */
+    Cursor queryForSingleItemAsMediaProvider(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, CancellationSignal signal)
+            throws FileNotFoundException {
+        final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
+        try {
+            return queryForSingleItem(uri, projection, selection, selectionArgs, signal);
+        } finally {
+            restoreLocalCallingIdentity(tokenInner);
+        }
+    }
+
+    /**
      * Query the given {@link Uri}, expecting only a single item to be found.
      *
      * @throws FileNotFoundException if no items were found, or multiple items
@@ -7070,33 +7090,49 @@
             final String[] projection = new String[]{
                     MediaColumns._ID,
                     MediaColumns.OWNER_PACKAGE_NAME,
-                    MediaColumns.IS_PENDING};
+                    MediaColumns.IS_PENDING,
+                    FileColumns.MEDIA_TYPE};
             final String selection = MediaColumns.DATA + "=?";
-            final String[] selectionArgs = new String[] { path };
-            final Uri fileUri;
+            final String[] selectionArgs = new String[]{ path };
+            final long id;
+            final int mediaType;
             final boolean isPending;
             String ownerPackageName = null;
-            try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
+            try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
+                    selection,
                     selectionArgs, null)) {
-                fileUri = ContentUris.withAppendedId(contentUri, c.getInt(0));
+                id = c.getLong(0);
                 ownerPackageName = c.getString(1);
                 isPending = c.getInt(2) != 0;
+                mediaType = c.getInt(3);
             }
-
             final File file = new File(path);
-            checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
-
+            final Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path), id);
             // We don't check ownership for files with IS_PENDING set by FUSE
             if (isPending && !isPendingFromFuse(new File(path))) {
                 requireOwnershipForItem(ownerPackageName, fileUri);
             }
+
+            // Check that path looks consistent before uri checks
+            if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
+                checkWorldReadAccess(file.getAbsolutePath());
+            }
+
+            try {
+                // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
+                // which we already check above. Hence, handling only SecurityException.
+                checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
+            } catch (SecurityException e) {
+                // Check for other Uri formats only when the single uri check flow fails.
+                // Throw the previous exception if the multi-uri checks failed.
+                if (!(isFilePathSupportForMediaUris() &&
+                        hasOtherUriGrants(path, mediaType, id, forWrite))) {
+                    throw e;
+                }
+            }
             return 0;
         } catch (FileNotFoundException e) {
-            // We are here because
-            // * App doesn't have read permission to the requested path, hence queryForSingleItem
-            //   couldn't return a valid db row, or,
-            // * There is no db row corresponding to the requested path, which is more unlikely.
-            // In both of these cases, it means that app doesn't have access permission to the file.
+            // We are here because there is no db row corresponding to the requested path.
             Log.e(TAG, "Couldn't find file: " + path);
             return OsConstants.EACCES;
         } catch (IllegalStateException | SecurityException e) {
@@ -7107,6 +7143,43 @@
         }
     }
 
+    private boolean hasOtherUriGrants(String path, int mediaType, long id, boolean forWrite) {
+        Set<Uri> otherUris = new ArraySet<Uri>();
+        final Uri mediaUri = getMediaUriForFuse(extractVolumeName(path), mediaType, id);
+        otherUris.add(mediaUri);
+        final Uri externalMediaUri = getMediaUriForFuse(MediaStore.VOLUME_EXTERNAL, mediaType, id);
+        otherUris.add(externalMediaUri);
+        return bulkCheckUriPermissions(otherUris, forWrite);
+    }
+
+    private @NonNull Uri getMediaUriForFuse(@NonNull String volumeName, int mediaType, long id) {
+        switch (mediaType) {
+            case FileColumns.MEDIA_TYPE_IMAGE:
+                return MediaStore.Images.Media.getContentUri(volumeName, id);
+            case FileColumns.MEDIA_TYPE_VIDEO:
+                return MediaStore.Video.Media.getContentUri(volumeName, id);
+            case FileColumns.MEDIA_TYPE_AUDIO:
+                return MediaStore.Audio.Media.getContentUri(volumeName, id);
+            case FileColumns.MEDIA_TYPE_PLAYLIST:
+                return ContentUris.withAppendedId(
+                        MediaStore.Audio.Playlists.getContentUri(volumeName), id);
+            default:
+                // return files URIs
+                return MediaStore.Files.getContentUri(volumeName, id);
+        }
+    }
+
+    /**
+     * Feature flag to support File APIs for different formats of media-store URI grants like:
+     *   * content://media/external_primary/images/media/123
+     *   * content://media/external/images/media/123
+     *
+     *   Default value: false
+     */
+    private boolean isFilePathSupportForMediaUris() {
+        return SystemProperties.getBoolean("sys.filepathsupport.mediauri", false);
+    }
+
     /**
      * Returns {@code true} if {@link #mCallingIdentity#getSharedPackages(String)} contains the
      * given package name, {@code false} otherwise.
@@ -7670,14 +7743,24 @@
         }
 
         // Outstanding grant means they get access
-        if (getContext().checkUriPermission(uri, mCallingIdentity.get().pid,
+        return isUriPermissionGranted(uri, forWrite);
+    }
+
+    private boolean bulkCheckUriPermissions(Set<Uri> uris, boolean forWrite) {
+        for (Uri uri : uris) {
+            if (isUriPermissionGranted(uri, forWrite)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isUriPermissionGranted(Uri uri, boolean forWrite) {
+        int uriPermission = getContext().checkUriPermission(uri, mCallingIdentity.get().pid,
                 mCallingIdentity.get().uid, forWrite
                         ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                        : Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) {
-            return true;
-        }
-
-        return false;
+                        : Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        return uriPermission == PERMISSION_GRANTED;
     }
 
     @VisibleForTesting