Merge "Rename local variables"
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 53323fb..2dca9fd 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -603,24 +603,29 @@
      */
     public final static String EXTRA_OUTPUT = "output";
 
-    /**
+    /*
      * Specify that the caller wants to receive the original media format without transcoding.
      *
-     * This is a very dangerous flag to use because apps can suddenly fail to play media after
-     * an OS upgrade. Clients should instead specify their supported media capabilities explicitly
-     * in their manifest or with the {@link #EXTRA_MEDIA_CAPABILITIES} open flag.
+     * <b>Caution: using this flag can cause app
+     * compatibility issues whenever Android adds support for new media formats.</b>
+     * Clients should instead specify their supported media capabilities explicitly
+     * in their manifest or with the {@link #EXTRA_MEDIA_CAPABILITIES} {@code open} flag.
+     *
+     * This option is useful for apps that don't attempt to parse the actual byte contents of media
+     * files, such as playback using {@link MediaPlayer} or for off-device backup. Note that the
+     * {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION} permission will still be required
+     * to avoid sensitive metadata redaction, similar to {@link #setRequireOriginal(Uri)}.
+     * </ul>
      *
      * Note that this flag overrides any explicitly declared {@code media_capabilities.xml} or
      * {@link ApplicationMediaCapabilities} extras specified in the same {@code open} request.
      *
-     * This is only useful for apps that are bundled with the system hence are guaranteed to
-     * always support any media capabilities released on the OS in the future.
-     *
      * <p>This option can be added to the {@code opts} {@link Bundle} in various
      * {@link ContentResolver} {@code open} methods.
      *
      * @see ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle)
      * @see ContentResolver#openTypedAssetFile(Uri, String, Bundle, CancellationSignal)
+     * @see #setRequireOriginal(Uri)
      */
     public final static String EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT =
             "android.provider.extra.ACCEPT_ORIGINAL_MEDIA_FORMAT";
diff --git a/jni/node-inl.h b/jni/node-inl.h
index aa067be..1fa62b0 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -179,8 +179,10 @@
     // associated with its descendants.
     std::string BuildSafePath() const;
 
-    // Looks up a direct descendant of this node by name. If |acquire| is true,
+    // Looks up a direct descendant of this node by case-insensitive |name|. If |acquire| is true,
     // also Acquire the node before returning a reference to it.
+    // |transforms| is an opaque flag that is used to distinguish multiple nodes sharing the same
+    // |name| but requiring different IO transformations as determined by the MediaProvider.
     node* LookupChildByName(const std::string& name, bool acquire, const int transforms = 0) const {
         return ForChild(name, [acquire, transforms](node* child) {
             if (child->transforms_ == transforms) {
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 95b5561..0183690 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -180,13 +180,26 @@
         }
 
         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setTitle(resolveTitleText());
+        // We set the title in message so that the text doesn't get truncated
+        builder.setMessage(resolveTitleText());
         builder.setPositiveButton(R.string.allow, this::onPositiveAction);
         builder.setNegativeButton(R.string.deny, this::onNegativeAction);
         builder.setCancelable(false);
         builder.setView(bodyView);
 
         actionDialog = builder.show();
+
+        // The title is being set as a message above.
+        // We need to style it like the default AlertDialog title
+        TextView dialogMessage = (TextView) actionDialog.findViewById(
+                android.R.id.message);
+        if (dialogMessage != null) {
+            dialogMessage.setTextAppearance(
+                    android.R.style.TextAppearance_DeviceDefault_DialogWindowTitle);
+        } else {
+            Log.w(TAG, "Couldn't find message element");
+        }
+
         final WindowManager.LayoutParams params = actionDialog.getWindow().getAttributes();
         params.width = getResources().getDimensionPixelSize(R.dimen.permission_dialog_width);
         actionDialog.getWindow().setAttributes(params);
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 46c3516..173eb26 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -690,11 +690,8 @@
                 actualMimeType = mDrmClient.getOriginalMimeType(realFile.getPath());
             }
 
-            int actualMediaType = FileColumns.MEDIA_TYPE_NONE;
-            if (actualMimeType != null) {
-                actualMediaType = resolveMediaTypeFromFilePath(realFile, actualMimeType,
-                        /*isHidden*/ mHiddenDirCount > 0);
-            }
+            int actualMediaType = mediaTypeFromMimeType(
+                    realFile, actualMimeType, FileColumns.MEDIA_TYPE_NONE);
 
             Trace.beginSection("checkChanged");
 
@@ -718,13 +715,9 @@
             try (Cursor c = mResolver.query(mFilesUri, projection, queryArgs, mSignal)) {
                 if (c.moveToFirst()) {
                     existingId = c.getLong(0);
-                    final long dateModified = c.getLong(1);
-                    final long size = c.getLong(2);
                     final String mimeType = c.getString(3);
                     final int mediaType = c.getInt(4);
                     isPendingFromFuse &= c.getInt(5) != 0;
-                    final boolean isScanned =
-                            c.getInt(6) == FileColumns._MODIFIER_MEDIA_SCAN;
 
                     // Remember visiting this existing item, even if we skipped
                     // due to it being unchanged; this is needed so we don't
@@ -736,18 +729,36 @@
                         mFirstId = existingId;
                     }
 
-                    final boolean sameTime = (lastModifiedTime(realFile, attrs) == dateModified);
-                    final boolean sameSize = (attrs.size() == size);
-                    final boolean sameMimeType = mimeType == null ? actualMimeType == null :
-                            mimeType.equalsIgnoreCase(actualMimeType);
-                    final boolean sameMediaType = (actualMediaType == mediaType);
-                    final boolean isSame = sameTime && sameSize && sameMediaType && sameMimeType
-                            && !isPendingFromFuse && isScanned;
-                    if (attrs.isDirectory() || isSame) {
+                    if (attrs.isDirectory()) {
+                        if (LOGV) Log.v(TAG, "Skipping directory " + file);
+                        return FileVisitResult.CONTINUE;
+                    }
+
+                    final boolean sameMetadata =
+                            hasSameMetadata(attrs, realFile, isPendingFromFuse, c);
+                    if (isSame(
+                            sameMetadata, actualMimeType, actualMediaType, mimeType, mediaType)) {
                         if (LOGV) Log.v(TAG, "Skipping unchanged " + file);
                         return FileVisitResult.CONTINUE;
                     }
+
+                    // For this special case we may have changed mime type from the file's metadata.
+                    // This is safe because mime_type cannot be changed outside of scanning.
+                    if (sameMetadata
+                            && "video/mp4".equalsIgnoreCase(actualMimeType)
+                            && "audio/mp4".equalsIgnoreCase(mimeType)) {
+                        if (LOGV) Log.v(TAG, "Skipping unchanged video/audio " + file);
+                        return FileVisitResult.CONTINUE;
+                    }
                 }
+
+                // Since we allow top-level mime type to be customised, we need to do this early
+                // on, so the file is later scanned as the appropriate type (otherwise, this
+                // audio filed would be scanned as video and it would be missing the correct
+                // metadata).
+                actualMimeType = updateM4aMimeType(realFile, actualMimeType);
+                actualMediaType =
+                        mediaTypeFromMimeType(realFile, actualMimeType, actualMediaType);
             } finally {
                 Trace.endSection();
             }
@@ -777,6 +788,65 @@
             return FileVisitResult.CONTINUE;
         }
 
+        private boolean isSame(
+                boolean hasSameMetadata,
+                String actualMimeType,
+                int actualMediaType,
+                String mimeType,
+                int mediaType) {
+            boolean sameMimeType =
+                    mimeType == null
+                            ? actualMimeType == null
+                            : mimeType.equalsIgnoreCase(actualMimeType);
+            boolean sameMediaType = (actualMediaType == mediaType);
+            return hasSameMetadata && sameMediaType && sameMimeType;
+        }
+
+        private int mediaTypeFromMimeType(
+                File file, String mimeType, int defaultMediaType) {
+            if (mimeType != null) {
+                return resolveMediaTypeFromFilePath(
+                        file, mimeType, /*isHidden*/ mHiddenDirCount > 0);
+            }
+            return defaultMediaType;
+        }
+
+        private boolean hasSameMetadata(
+                BasicFileAttributes attrs, File realFile, boolean isPendingFromFuse, Cursor c) {
+            final long dateModified = c.getLong(1);
+            final boolean sameTime = (lastModifiedTime(realFile, attrs) == dateModified);
+
+            final long size = c.getLong(2);
+            final boolean sameSize = (attrs.size() == size);
+
+            final boolean isScanned =
+                    c.getInt(6) == FileColumns._MODIFIER_MEDIA_SCAN;
+
+            return sameTime && sameSize && !isPendingFromFuse && isScanned;
+        }
+
+        /**
+         * For this one very narrow case, we allow mime types to be customised when the top levels
+         * differ. This opens the given file, so avoid calling unless really necessary. This
+         * returns the defaultMimeType for non-m4a files or if opening the file throws an exception.
+         */
+        private String updateM4aMimeType(File file, String defaultMimeType) {
+            if ("video/mp4".equalsIgnoreCase(defaultMimeType)) {
+                try (
+                    FileInputStream is = new FileInputStream(file);
+                    MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
+                    mmr.setDataSource(is.getFD());
+                    String refinedMimeType = mmr.extractMetadata(METADATA_KEY_MIMETYPE);
+                    if ("audio/mp4".equalsIgnoreCase(refinedMimeType)) {
+                        return refinedMimeType;
+                    }
+                } catch (Exception e) {
+                    return defaultMimeType;
+                }
+            }
+            return defaultMimeType;
+        }
+
         @Override
         public FileVisitResult visitFileFailed(Path file, IOException exc)
                 throws IOException {
@@ -1537,12 +1607,6 @@
 
         if (fileMimeType.regionMatches(true, 0, refinedMimeType, 0, refinedSplit + 1)) {
             return Optional.of(refinedMimeType);
-        } else if ("video/mp4".equalsIgnoreCase(fileMimeType)
-                && "audio/mp4".equalsIgnoreCase(refinedMimeType)) {
-            // We normally only allow MIME types to be customized when the
-            // top-level type agrees, but this one very narrow case is added to
-            // support a music service that was writing "m4a" files as "mp4".
-            return Optional.of(refinedMimeType);
         } else {
             return Optional.empty();
         }
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index 6102464..753547b 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -146,21 +146,10 @@
         assertTrue(parseOptionalMimeType("image/png", "image/x-shiny").isPresent());
         assertEquals("image/x-shiny",
                 parseOptionalMimeType("image/png", "image/x-shiny").get());
-    }
 
-    @Test
-    public void testOverrideMimeType_148316354() throws Exception {
         // Radical file type shifting isn't allowed
         assertEquals(Optional.empty(),
                 parseOptionalMimeType("video/mp4", "audio/mpeg"));
-
-        // One specific narrow type of shift (mp4 -> m4a) is allowed
-        assertEquals(Optional.of("audio/mp4"),
-                parseOptionalMimeType("video/mp4", "audio/mp4"));
-
-        // The other direction isn't allowed
-        assertEquals(Optional.empty(),
-                parseOptionalMimeType("audio/mp4", "video/mp4"));
     }
 
     @Test
@@ -900,8 +889,11 @@
                 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
             assertEquals(1, cursor.getCount());
             cursor.moveToFirst();
-            assertEquals("audio/mp4",
-                    cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)));
+            assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)))
+                    .isEqualTo("audio/mp4");
+            assertThat(cursor.getString(cursor.getColumnIndex(AudioColumns.IS_MUSIC)))
+                    .isEqualTo("1");
+
         }
     }