Merge "Test that _modifier defaults to MODIFIER_MEDIA_SCAN after db upgrade" into sc-dev
diff --git a/Android.bp b/Android.bp
index fa33f8f..405f33c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -7,6 +7,7 @@
"androidx.appcompat_appcompat",
"androidx.core_core",
"guava",
+ "modules-utils-build",
],
libs: [
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index 623780a..0426cb0 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -12,6 +12,7 @@
method public static long getGeneration(@NonNull android.content.Context, @NonNull String);
method public static android.net.Uri getMediaScannerUri();
method @Nullable public static android.net.Uri getMediaUri(@NonNull android.content.Context, @NonNull android.net.Uri);
+ method @NonNull public static android.os.ParcelFileDescriptor getOriginalMediaFormatFileDescriptor(@NonNull android.content.Context, @NonNull android.os.ParcelFileDescriptor) throws java.io.IOException;
method @NonNull public static java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context);
method public static boolean getRequireOriginal(@NonNull android.net.Uri);
method @NonNull public static String getVersion(@NonNull android.content.Context);
@@ -34,6 +35,7 @@
field public static final String EXTRA_MEDIA_ALBUM = "android.intent.extra.album";
field public static final String EXTRA_MEDIA_ARTIST = "android.intent.extra.artist";
field public static final String EXTRA_MEDIA_CAPABILITIES = "android.provider.extra.MEDIA_CAPABILITIES";
+ field public static final String EXTRA_MEDIA_CAPABILITIES_UID = "android.provider.extra.MEDIA_CAPABILITIES_UID";
field public static final String EXTRA_MEDIA_FOCUS = "android.intent.extra.focus";
field public static final String EXTRA_MEDIA_GENRE = "android.intent.extra.genre";
field public static final String EXTRA_MEDIA_PLAYLIST = "android.intent.extra.playlist";
@@ -135,6 +137,7 @@
field public static final String IS_MUSIC = "is_music";
field public static final String IS_NOTIFICATION = "is_notification";
field public static final String IS_PODCAST = "is_podcast";
+ field public static final String IS_RECORDING = "is_recording";
field public static final String IS_RINGTONE = "is_ringtone";
field @Deprecated public static final String TITLE_KEY = "title_key";
field public static final String TITLE_RESOURCE_URI = "title_resource_uri";
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 2d6df79..4a12ce1 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -48,12 +48,14 @@
import android.media.ExifInterface;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.OperationCanceledException;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
@@ -194,6 +196,10 @@
public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration";
/** {@hide} */
+ public static final String GET_ORIGINAL_MEDIA_FORMAT_FILE_DESCRIPTOR_CALL =
+ "get_original_media_format_file_descriptor";
+
+ /** {@hide} */
@Deprecated
public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
"com.android.externalstorage.documents";
@@ -215,6 +221,9 @@
/** {@hide} */
public static final String EXTRA_RESULT = "result";
+ /** {@hide} */
+ public static final String EXTRA_FILE_DESCRIPTOR = "file_descriptor";
+
/**
* This is for internal use by the media scanner only.
* Name of the (optional) Uri parameter that determines whether to skip deleting
@@ -603,7 +612,7 @@
*/
public final static String EXTRA_OUTPUT = "output";
- /*
+ /**
* Specify that the caller wants to receive the original media format without transcoding.
*
* <b>Caution: using this flag can cause app
@@ -626,6 +635,7 @@
* @see ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle)
* @see ContentResolver#openTypedAssetFile(Uri, String, Bundle, CancellationSignal)
* @see #setRequireOriginal(Uri)
+ * @see MediaStore#getOriginalMediaFormatFileDescriptor(Context, ParcelFileDescriptor)
*/
public final static String EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT =
"android.provider.extra.ACCEPT_ORIGINAL_MEDIA_FORMAT";
@@ -650,6 +660,17 @@
"android.provider.extra.MEDIA_CAPABILITIES";
/**
+ * Specify the UID of the app that should be used to determine supported media capabilities
+ * while opening a media.
+ *
+ * If this specified UID is found to be capable of handling the original media file format, the
+ * app will receive the original file, otherwise, the file will get transcoded to a default
+ * format supported by the specified UID.
+ */
+ public static final String EXTRA_MEDIA_CAPABILITIES_UID =
+ "android.provider.extra.MEDIA_CAPABILITIES_UID";
+
+ /**
* The string that is used when a media attribute is not known. For example,
* if an audio file does not have any meta data, the artist and album columns
* will be set to this value.
@@ -838,6 +859,30 @@
}
/**
+ * Returns {@link ParcelFileDescriptor} representing the original media file format for
+ * {@code fileDescriptor}.
+ *
+ * <p>Media files may get transcoded based on an application's media capabilities requirements.
+ * However, in various cases, when the application needs access to the original media file, or
+ * doesn't attempt to parse the actual byte contents of media files, such as playback using
+ * {@link MediaPlayer} or for off-device backup, this method can be useful.
+ *
+ * @throws IOException if the given {@link ParcelFileDescriptor} could not be converted
+ *
+ * @see MediaStore#EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT
+ */
+ public static @NonNull ParcelFileDescriptor getOriginalMediaFormatFileDescriptor(
+ @NonNull Context context,
+ @NonNull ParcelFileDescriptor fileDescriptor) throws IOException {
+ Bundle input = new Bundle();
+ input.putParcelable(EXTRA_FILE_DESCRIPTOR, fileDescriptor);
+
+ Bundle output = context.getContentResolver().call(AUTHORITY,
+ GET_ORIGINAL_MEDIA_FORMAT_FILE_DESCRIPTOR_CALL, null, input);
+ return output.getParcelable(EXTRA_FILE_DESCRIPTOR);
+ }
+
+ /**
* Rewrite the given {@link Uri} to point at
* {@link MediaStore#AUTHORITY_LEGACY}.
*
@@ -2708,6 +2753,12 @@
public static final String IS_AUDIOBOOK = "is_audiobook";
/**
+ * Non-zero if the audio file is a recording
+ */
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+ public static final String IS_RECORDING = "is_recording";
+
+ /**
* The id of the genre the audio file is from, if any
*/
@Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
diff --git a/legacy/Android.bp b/legacy/Android.bp
index 203ee5a..89ef09c 100644
--- a/legacy/Android.bp
+++ b/legacy/Android.bp
@@ -7,6 +7,7 @@
"androidx.appcompat_appcompat",
"androidx.core_core",
"guava",
+ "modules-utils-build",
],
libs: ["app-compat-annotations"],
@@ -17,7 +18,7 @@
":mediaprovider-database-sources",
],
+ platform_apis: true,
certificate: "media",
privileged: true,
- sdk_version: "system_current",
}
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 5a21531..6064d18 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -60,6 +60,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.playlist.Playlist;
import com.android.providers.media.util.BackgroundThread;
import com.android.providers.media.util.DatabaseUtils;
@@ -813,7 +814,7 @@
+ "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
+ "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL,"
+ "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL,"
- + "_modifier INTEGER DEFAULT 0)");
+ + "_modifier INTEGER DEFAULT 0, is_recording INTEGER DEFAULT 0)");
db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
if (!mInternal) {
@@ -1370,6 +1371,14 @@
db.execSQL("ALTER TABLE files ADD COLUMN is_audiobook INTEGER DEFAULT 0;");
}
+ private static void updateAddRecording(SQLiteDatabase db, boolean internal) {
+ db.execSQL("ALTER TABLE files ADD COLUMN is_recording INTEGER DEFAULT 0;");
+ if (SdkLevel.isAtLeastS()) {
+ // We add the column is_recording, rescan all music files
+ db.execSQL("UPDATE files SET date_modified=0 WHERE is_music=1;");
+ }
+ }
+
private static void updateClearLocation(SQLiteDatabase db, boolean internal) {
db.execSQL("UPDATE files SET latitude=NULL, longitude=NULL;");
}
@@ -1509,6 +1518,8 @@
private static void updateAddModifier(SQLiteDatabase db, boolean internal) {
db.execSQL("ALTER TABLE files ADD COLUMN _modifier INTEGER DEFAULT 0;");
+ // For existing files, set default value as _MODIFIER_MEDIA_SCAN
+ db.execSQL("UPDATE files SET _modifier=3;");
}
private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
@@ -1573,7 +1584,7 @@
static final int VERSION_R = 1115;
// Leave some gaps in database version tagging to allow R schema changes
// to go independent of S schema changes.
- static final int VERSION_S = 1204;
+ static final int VERSION_S = 1205;
static final int VERSION_LATEST = VERSION_S;
/**
@@ -1734,6 +1745,9 @@
if (fromVersion < 1204) {
// Empty version bump to ensure views are recreated
}
+ if (fromVersion < 1205) {
+ updateAddRecording(db, internal);
+ }
// If this is the legacy database, it's not worth recomputing data
// values locally, since they'll be recomputed after the migration
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 92150f6..7d5395a 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -328,7 +328,7 @@
return true;
}
- return checkIsLegacyStorageGranted(context, uid, getPackageName());
+ return checkIsLegacyStorageGranted(context, uid, getPackageName(), attributionTag);
}
private boolean isScopedStorageEnforced(boolean defaultScopedStorage,
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 222b9ea..c450f9c 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -71,6 +71,7 @@
import com.android.providers.media.util.FileUtils;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -1018,6 +1019,7 @@
throws FileNotFoundException {
enforceShellRestrictions();
final Uri target = getUriForDocumentId(docId);
+ final int callingUid = Binder.getCallingUid();
if (!"r".equals(mode)) {
throw new IllegalArgumentException("Media is read-only");
@@ -1026,12 +1028,27 @@
// Delegate to real provider
final long token = Binder.clearCallingIdentity();
try {
- return getContext().getContentResolver().openFileDescriptor(target, mode);
+ return openFileForRead(target, callingUid);
} finally {
Binder.restoreCallingIdentity(token);
}
}
+ public ParcelFileDescriptor openFileForRead(final Uri target, final int callingUid)
+ throws FileNotFoundException {
+ final Bundle opts = new Bundle();
+ opts.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID, callingUid);
+
+ AssetFileDescriptor afd =
+ getContext().getContentResolver().openTypedAssetFileDescriptor(target, "*/*",
+ opts);
+ if (afd == null) {
+ return null;
+ }
+
+ return afd.getParcelFileDescriptor();
+ }
+
@Override
public AssetFileDescriptor openDocumentThumbnail(
String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index e93787e..a7839aa 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -177,6 +177,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
import com.android.providers.media.fuse.ExternalStorageServiceImpl;
@@ -263,6 +264,7 @@
private static final String DIRECTORY_DCIM_LOWER_CASE = "dcim";
private static final String DIRECTORY_DOCUMENTS_LOWER_CASE = "documents";
private static final String DIRECTORY_AUDIOBOOKS_LOWER_CASE = "audiobooks";
+ private static final String DIRECTORY_RECORDINGS_LOWER_CASE = "recordings";
private static final String DIRECTORY_ANDROID_LOWER_CASE = "android";
private static final String DIRECTORY_MEDIA = "media";
@@ -1371,7 +1373,7 @@
public boolean transformForFuse(String src, String dst, int transforms, int transformsReason,
int uid) {
if ((transforms & FLAG_TRANSFORM_TRANSCODING) != 0) {
- if (mTranscodeHelper.isTranscodeFileCached(uid, src, dst)) {
+ if (mTranscodeHelper.isTranscodeFileCached(src, dst)) {
Log.d(TAG, "Using transcode cache for " + src);
return true;
}
@@ -1932,16 +1934,25 @@
return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
}
+ private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
+ @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
+ @NonNull Bundle qbExtras) {
+ return updateDatabaseForFuseRename(helper, oldPath, newPath, values, qbExtras,
+ FileUtils.getContentUriForPath(oldPath));
+ }
+
/**
* Updates database entry for given {@code path} with {@code values}
*/
private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
@NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
- @NonNull Bundle qbExtras) {
- final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
+ @NonNull Bundle qbExtras, Uri uriOldPath) {
boolean allowHidden = isCallingPackageAllowedHidden();
final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null);
+ if (values.containsKey(FileColumns._MODIFIER)) {
+ qbForUpdate.allowColumn(FileColumns._MODIFIER);
+ }
final String selection = MediaColumns.DATA + " =? ";
int count = 0;
boolean retryUpdateWithReplace = false;
@@ -1976,7 +1987,7 @@
* Gets {@link ContentValues} for updating database entry to {@code path}.
*/
private ContentValues getContentValuesForFuseRename(String path, String newMimeType,
- boolean wasHidden, boolean isHidden) {
+ boolean wasHidden, boolean isHidden, boolean isSameMimeType) {
ContentValues values = new ContentValues();
values.put(MediaColumns.MIME_TYPE, newMimeType);
values.put(MediaColumns.DATA, path);
@@ -1986,15 +1997,15 @@
} else {
int mediaType = MimeUtils.resolveMediaType(newMimeType);
values.put(FileColumns.MEDIA_TYPE, mediaType);
- if (wasHidden) {
- // Set this as pending so that apps can scan the file to update the metadata.
- // Otherwise, scan will skip scanning this file because rename() doesn't change
- // lastModifiedTime and scan assumes there is no change in the file.
- // This should be safe because, in Q, apps had to insert new db row or scan the file
- // to insert this file to database.
- values.put(FileColumns.IS_PENDING, 1);
- }
}
+
+ if ((!isHidden && wasHidden) || !isSameMimeType) {
+ // Set the modifier as MODIFIER_FUSE so that apps can scan the file to update the
+ // metadata. Otherwise, scan will skip scanning this file because rename() doesn't
+ // change lastModifiedTime and scan assumes there is no change in the file.
+ values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
+ }
+
final boolean allowHidden = isCallingPackageAllowedHidden();
if (!newMimeType.equalsIgnoreCase("null") &&
matchUri(getContentUriForFile(path, newMimeType), allowHidden) == AUDIO_MEDIA) {
@@ -2184,7 +2195,8 @@
final String newFilePath = newPath + "/" + filePath;
final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
- getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden),
+ getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden,
+ /* isSameMimeType */ true),
qbExtras)) {
Log.e(TAG, "Calling package doesn't have write permission to rename file.");
return OsConstants.EPERM;
@@ -2264,11 +2276,19 @@
helper.beginTransaction();
try {
final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
- if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
- getContentValuesForFuseRename(newPath, newMimeType, wasHidden, isHidden))) {
+ final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
+ final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType);
+ final ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
+ wasHidden, isHidden, isSameMimeType);
+
+ if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) {
if (!bypassRestrictions) {
- Log.e(TAG, "Calling package doesn't have write permission to rename file.");
- return OsConstants.EPERM;
+ // Check for other URI format grants for oldPath only. Check right before
+ // returning EPERM, to leave positive case performance unaffected.
+ if (!renameWithOtherUriGrants(helper, oldPath, newPath, contentValues)) {
+ Log.e(TAG, "Calling package doesn't have write permission to rename file.");
+ return OsConstants.EPERM;
+ }
} else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
return OsConstants.EPERM;
@@ -2306,6 +2326,21 @@
}
/**
+ * Rename file by checking for other URI grants on oldPath
+ *
+ * We don't support replace scenario by checking for other URI grants on newPath (if it exists).
+ */
+ private boolean renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath,
+ ContentValues contentValues) {
+ final Uri oldPathGrantedUri = getOtherUriGrantsForPath(oldPath, /* forWrite */ true);
+ if (oldPathGrantedUri == null) {
+ return false;
+ }
+ return updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues, Bundle.EMPTY,
+ oldPathGrantedUri);
+ }
+
+ /**
* Rename file/directory without imposing any restrictions.
*
* We don't impose any rename restrictions for apps that bypass scoped storage restrictions.
@@ -2741,13 +2776,24 @@
defaultMimeType = "audio/mpeg";
defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO;
defaultPrimary = Environment.DIRECTORY_MUSIC;
- allowedPrimary = Arrays.asList(
- Environment.DIRECTORY_ALARMS,
- Environment.DIRECTORY_AUDIOBOOKS,
- Environment.DIRECTORY_MUSIC,
- Environment.DIRECTORY_NOTIFICATIONS,
- Environment.DIRECTORY_PODCASTS,
- Environment.DIRECTORY_RINGTONES);
+ if (SdkLevel.isAtLeastS()) {
+ allowedPrimary = Arrays.asList(
+ Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_AUDIOBOOKS,
+ Environment.DIRECTORY_MUSIC,
+ Environment.DIRECTORY_NOTIFICATIONS,
+ Environment.DIRECTORY_PODCASTS,
+ Environment.DIRECTORY_RECORDINGS,
+ Environment.DIRECTORY_RINGTONES);
+ } else {
+ allowedPrimary = Arrays.asList(
+ Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_AUDIOBOOKS,
+ Environment.DIRECTORY_MUSIC,
+ Environment.DIRECTORY_NOTIFICATIONS,
+ Environment.DIRECTORY_PODCASTS,
+ Environment.DIRECTORY_RINGTONES);
+ }
break;
case VIDEO_MEDIA:
case VIDEO_MEDIA_ID:
@@ -4947,7 +4993,7 @@
// Check for other URI format grants for File API call only. Check right before
// returning count = 0, to leave positive cases performance unaffected.
- if (count == 0 && isFuseThread() && isFilePathSupportForMediaUris()) {
+ if (count == 0 && isFuseThread()) {
count += deleteWithOtherUriGrants(uri, helper, projection, userWhere, userWhereArgs,
extras);
}
@@ -5195,6 +5241,22 @@
res.putParcelable(MediaStore.EXTRA_RESULT, pi);
return res;
}
+ case MediaStore.GET_ORIGINAL_MEDIA_FORMAT_FILE_DESCRIPTOR_CALL: {
+ ParcelFileDescriptor inputPfd =
+ extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR);
+ try {
+ File file = getFileFromFileDescriptor(inputPfd);
+ FuseDaemon fuseDaemon = getFuseDaemonForFile(file);
+
+ ParcelFileDescriptor outputPfd =
+ fuseDaemon.getOriginalMediaFormatFileDescriptor(inputPfd);
+ Bundle res = new Bundle();
+ res.putParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR, outputPfd);
+ return res;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
default:
throw new UnsupportedOperationException("Unsupported call: " + method);
}
@@ -5209,6 +5271,26 @@
}
/**
+ * Return the filesystem path of the real file on disk that is represented
+ * by the given {@link ParcelFileDescriptor}.
+ *
+ * Copied from {@link ParcelFileDescriptor#getFile}
+ */
+ private static File getFileFromFileDescriptor(ParcelFileDescriptor fileDescriptor)
+ throws IOException {
+ try {
+ final String path = Os.readlink("/proc/self/fd/" + fileDescriptor.getFd());
+ if (OsConstants.S_ISREG(Os.stat(path).st_mode)) {
+ return new File(path);
+ } else {
+ throw new IOException("Not a regular file: " + path);
+ }
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ }
+
+ /**
* Generate the {@link PendingIntent} for the given grant request. This
* method also checks the incoming arguments for security purposes
* before creating the privileged {@link PendingIntent}.
@@ -7349,19 +7431,20 @@
try {
if (isPrivatePackagePathNotAccessibleByCaller(path)) {
Log.e(TAG, "Can't open a file in another app's external directory!");
- return new FileOpenResult(OsConstants.ENOENT, uid, new long[0]);
+ return new FileOpenResult(OsConstants.ENOENT, original_uid, new long[0]);
}
if (shouldBypassFuseRestrictions(forWrite, path)) {
isSuccess = true;
- return new FileOpenResult(0 /* status */, uid,
+ return new FileOpenResult(0 /* status */, original_uid,
redact ? getRedactionRangesForFuse(path, ioPath, original_uid, uid, tid) :
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
if (isCallingPackageRequestingLegacy()) {
- return new FileOpenResult(OsConstants.EACCES /* status */, uid, new long[0]);
+ return new FileOpenResult(OsConstants.EACCES /* status */, original_uid,
+ new long[0]);
}
final Uri contentUri = FileUtils.getContentUriForPath(path);
@@ -7403,13 +7486,12 @@
} 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() &&
- getOtherUriGrantsForPath(path, mediaType, id, forWrite) != null)) {
+ if (getOtherUriGrantsForPath(path, mediaType, id, forWrite) == null) {
throw e;
}
}
isSuccess = true;
- return new FileOpenResult(0 /* status */, uid,
+ return new FileOpenResult(0 /* status */, original_uid,
redact ? getRedactionRangesForFuse(path, ioPath, original_uid, uid, tid) :
new long[0]);
} catch (IOException e) {
@@ -7418,10 +7500,10 @@
// * getRedactionRangesForFuse couldn't fetch the redaction info correctly
// In all of these cases, it means that app doesn't have access permission to the file.
Log.e(TAG, "Couldn't find file: " + path, e);
- return new FileOpenResult(OsConstants.EACCES /* status */, uid, new long[0]);
+ return new FileOpenResult(OsConstants.EACCES /* status */, original_uid, new long[0]);
} catch (IllegalStateException | SecurityException e) {
Log.e(TAG, "Permission to access file: " + path + " is denied");
- return new FileOpenResult(OsConstants.EACCES /* status */, uid, new long[0]);
+ return new FileOpenResult(OsConstants.EACCES /* status */, original_uid, new long[0]);
} finally {
if (isSuccess && logTransformsMetrics) {
notifyTranscodeHelperOnFileOpen(path, ioPath, original_uid, transformsReason);
@@ -7430,6 +7512,25 @@
}
}
+ private @Nullable Uri getOtherUriGrantsForPath(String path, boolean forWrite) {
+ final Uri contentUri = FileUtils.getContentUriForPath(path);
+ final String[] projection = new String[]{
+ MediaColumns._ID,
+ FileColumns.MEDIA_TYPE};
+ final String selection = MediaColumns.DATA + "=?";
+ final String[] selectionArgs = new String[]{ path };
+ final long id;
+ final int mediaType;
+ try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, selection,
+ selectionArgs, null)) {
+ id = c.getLong(0);
+ mediaType = c.getInt(1);
+ return getOtherUriGrantsForPath(path, mediaType, id, forWrite);
+ } catch (FileNotFoundException ignored) {
+ }
+ return null;
+ }
+
private @Nullable Uri getOtherUriGrantsForPath(String path, int mediaType, long id,
boolean forWrite) {
Set<Uri> otherUris = new ArraySet<Uri>();
@@ -7458,17 +7559,6 @@
}
/**
- * 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.
* <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
@@ -7510,6 +7600,7 @@
case DIRECTORY_ALARMS_LOWER_CASE:
case DIRECTORY_NOTIFICATIONS_LOWER_CASE:
case DIRECTORY_AUDIOBOOKS_LOWER_CASE:
+ case DIRECTORY_RECORDINGS_LOWER_CASE:
uri = Audio.Media.getContentUri(volName);
break;
case DIRECTORY_MUSIC_LOWER_CASE:
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index c830a5f..4377856 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -61,6 +61,7 @@
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video.VideoColumns;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -382,6 +383,21 @@
return transcodePath;
}
+ private static int getMediaCapabilitiesUid(int uid, Bundle bundle) {
+ if (bundle == null) {
+ return uid;
+ }
+ int mediaCapabilitiesUid = bundle.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID);
+ if (mediaCapabilitiesUid >= Process.FIRST_APPLICATION_UID) {
+ logVerbose(
+ "Media capabilities uid " + mediaCapabilitiesUid + ", passed for uid " + uid);
+ uid = mediaCapabilitiesUid;
+ } else {
+ logVerbose("Ignoring invalid Media capabilities uid " + mediaCapabilitiesUid);
+ }
+ return uid;
+ }
+
// TODO(b/173491972): Generalize to consider other file/app media capabilities beyond hevc
/**
* @return 0 or >0 representing whether we should transcode or not.
@@ -404,6 +420,8 @@
logVerbose("Transcode not enabled");
return 0;
}
+
+ uid = getMediaCapabilitiesUid(uid, bundle);
logVerbose("Checking shouldTranscode for: " + path + ". Uid: " + uid);
if (!supportsTranscode(path) || uid < Process.FIRST_APPLICATION_UID
@@ -422,6 +440,7 @@
if (fileFlags == 0) {
// Nothing to transcode
+ logVerbose("File is not HEVC");
return 0;
}
@@ -566,26 +585,39 @@
}
private int getFileFlags(String path) {
- try (Cursor cursor = queryFileForTranscode(path,
- new String[]{FileColumns._VIDEO_CODEC_TYPE})) {
+ final String[] projection = new String[] {
+ FileColumns._VIDEO_CODEC_TYPE,
+ VideoColumns.COLOR_STANDARD,
+ VideoColumns.COLOR_TRANSFER
+ };
+
+ try (Cursor cursor = queryFileForTranscode(path, projection)) {
if (cursor == null || !cursor.moveToNext()) {
logVerbose("Couldn't find database row");
return 0;
}
+ int result = 0;
if (isHevc(cursor.getString(0))) {
- return FLAG_HEVC;
- } else {
- logVerbose("File is not HEVC");
- return 0;
+ result |= FLAG_HEVC;
}
+ if (isHdr10Plus(cursor.getInt(1), cursor.getInt(2))) {
+ result |= FLAG_HDR_10_PLUS;
+ }
+ return result;
}
}
- private boolean isHevc(String mimeType) {
+ private static boolean isHevc(String mimeType) {
return MediaFormat.MIMETYPE_VIDEO_HEVC.equalsIgnoreCase(mimeType);
}
+ private static boolean isHdr10Plus(int colorStandard, int colorTransfer) {
+ return (colorStandard == MediaFormat.COLOR_STANDARD_BT2020) &&
+ (colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084
+ || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG);
+ }
+
public boolean supportsTranscode(String path) {
File file = new File(path);
String name = file.getName();
@@ -782,7 +814,7 @@
c.getLong(2) /* video_duration */,
c.getLong(3) /* capture_framerate */,
-1 /* transcode_reason */);
- } else if (isTranscodeFileCached(uid, path, ioPath)) {
+ } else if (isTranscodeFileCached(path, ioPath)) {
MediaProviderStatsLog.write(
TRANSCODING_DATA,
getNameForUid(uid) /* owner_package_name */,
@@ -801,7 +833,7 @@
}
}
- public boolean isTranscodeFileCached(int uid, String path, String transcodePath) {
+ public boolean isTranscodeFileCached(String path, String transcodePath) {
if (SystemProperties.getBoolean("sys.fuse.disable_transcode_cache", false)) {
// Caching is disabled. Hence, delete the cached transcode file.
return false;
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 9a433c9..1ce6d04 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -151,6 +151,12 @@
}
}
+ public ParcelFileDescriptor getOriginalMediaFormatFileDescriptor(
+ ParcelFileDescriptor fileDescriptor) {
+ // TODO (b/170488060): Implement get original media file fd via native fuse.
+ throw new UnsupportedOperationException();
+ }
+
private native long native_new(MediaProvider mediaProvider);
// Takes ownership of the passed in file descriptor!
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index f7fa001..c88fc20 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -90,6 +90,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.ExifUtils;
import com.android.providers.media.util.FileUtils;
@@ -736,8 +737,8 @@
final boolean sameMetadata =
hasSameMetadata(attrs, realFile, isPendingFromFuse, c);
- if (isSame(
- sameMetadata, actualMimeType, actualMediaType, mimeType, mediaType)) {
+ final boolean sameMediaType = actualMediaType == mediaType;
+ if (sameMetadata && sameMediaType) {
if (LOGV) Log.v(TAG, "Skipping unchanged " + file);
return FileVisitResult.CONTINUE;
}
@@ -750,21 +751,6 @@
if (LOGV) Log.v(TAG, "Skipping unchanged video/audio " + file);
return FileVisitResult.CONTINUE;
}
-
- // "audio/mp4" mime types can come from various extensions (e.g. 3ga, m4a). We
- // want to avoid unnecessary scans of these files (it takes a long time for
- // many files, e.g. on phone reboot), so we check the mime type from the file's
- // metadata. We avoid always checking the file's metadata (and only do it for
- // this narrow case) since it involves expensive file operations.
- if (sameMetadata
- && (actualMediaType == mediaType)
- && "audio/mp4".equalsIgnoreCase(mimeType)) {
- actualMimeType = getAudioMimeTypeFromMetadata(realFile, actualMimeType);
- if (mimeType.equalsIgnoreCase(actualMimeType)) {
- if (LOGV) Log.v(TAG, "Skipping unchanged audio/mp4 " + file);
- return FileVisitResult.CONTINUE;
- }
- }
}
// Since we allow top-level mime type to be customised, we need to do this early
@@ -803,20 +789,6 @@
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) {
@@ -862,25 +834,6 @@
return defaultMimeType;
}
- /**
- * Returns the mime type as read from the metadata of the given file or the given default
- * value if we cannot read the metadata. We want to avoid calling this during sanning,
- * since it involves expensive file operations.
- */
- private String getAudioMimeTypeFromMetadata(File file, String defaultMimeType) {
- try (
- FileInputStream is = new FileInputStream(file);
- MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
- mmr.setDataSource(is.getFD());
- Optional<String> optionalMimeType =
- parseOptionalMimeType(
- defaultMimeType, mmr.extractMetadata(METADATA_KEY_MIMETYPE));
- return optionalMimeType.orElse(defaultMimeType);
- } catch (Exception e) {
- return defaultMimeType;
- }
- }
-
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc)
throws IOException {
@@ -1240,6 +1193,9 @@
sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST);
sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK);
sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC);
+ if (SdkLevel.isAtLeastS()) {
+ sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
+ }
}
private static @NonNull ContentProviderOperation.Builder scanItemAudio(long existingId,
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 0d6494b..7e79091 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -64,6 +64,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
@@ -932,19 +934,39 @@
"(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:obb)(/?.*)");
@VisibleForTesting
- public static final String[] DEFAULT_FOLDER_NAMES = {
- Environment.DIRECTORY_MUSIC,
- Environment.DIRECTORY_PODCASTS,
- Environment.DIRECTORY_RINGTONES,
- Environment.DIRECTORY_ALARMS,
- Environment.DIRECTORY_NOTIFICATIONS,
- Environment.DIRECTORY_PICTURES,
- Environment.DIRECTORY_MOVIES,
- Environment.DIRECTORY_DOWNLOADS,
- Environment.DIRECTORY_DCIM,
- Environment.DIRECTORY_DOCUMENTS,
- Environment.DIRECTORY_AUDIOBOOKS,
- };
+ public static final String[] DEFAULT_FOLDER_NAMES;
+ static {
+ if (SdkLevel.isAtLeastS()) {
+ DEFAULT_FOLDER_NAMES = new String[]{
+ Environment.DIRECTORY_MUSIC,
+ Environment.DIRECTORY_PODCASTS,
+ Environment.DIRECTORY_RINGTONES,
+ Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_NOTIFICATIONS,
+ Environment.DIRECTORY_PICTURES,
+ Environment.DIRECTORY_MOVIES,
+ Environment.DIRECTORY_DOWNLOADS,
+ Environment.DIRECTORY_DCIM,
+ Environment.DIRECTORY_DOCUMENTS,
+ Environment.DIRECTORY_AUDIOBOOKS,
+ Environment.DIRECTORY_RECORDINGS,
+ };
+ } else {
+ DEFAULT_FOLDER_NAMES = new String[]{
+ Environment.DIRECTORY_MUSIC,
+ Environment.DIRECTORY_PODCASTS,
+ Environment.DIRECTORY_RINGTONES,
+ Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_NOTIFICATIONS,
+ Environment.DIRECTORY_PICTURES,
+ Environment.DIRECTORY_MOVIES,
+ Environment.DIRECTORY_DOWNLOADS,
+ Environment.DIRECTORY_DCIM,
+ Environment.DIRECTORY_DOCUMENTS,
+ Environment.DIRECTORY_AUDIOBOOKS,
+ };
+ }
+ }
/**
* Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index c52aa8b..d9765ac 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -78,13 +78,9 @@
*/
public static boolean checkPermissionManager(@NonNull Context context, int pid,
int uid, @NonNull String packageName, @Nullable String attributionTag) {
- if (checkPermissionForDataDelivery(context, MANAGE_EXTERNAL_STORAGE, pid, uid,
+ return checkPermissionForDataDelivery(context, MANAGE_EXTERNAL_STORAGE, pid, uid,
packageName, attributionTag,
- generateAppOpMessage(packageName,sOpDescription.get()))) {
- return true;
- }
- // Fallback to OPSTR_NO_ISOLATED_STORAGE app op.
- return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
+ generateAppOpMessage(packageName,sOpDescription.get()));
}
/**
@@ -118,10 +114,14 @@
generateAppOpMessage(packageName,sOpDescription.get()));
}
- public static boolean checkIsLegacyStorageGranted(
- @NonNull Context context, int uid, String packageName) {
- return context.getSystemService(AppOpsManager.class)
- .unsafeCheckOp(OPSTR_LEGACY_STORAGE, uid, packageName) == MODE_ALLOWED;
+ public static boolean checkIsLegacyStorageGranted(@NonNull Context context, int uid,
+ String packageName, @Nullable String attributionTag) {
+ if (context.getSystemService(AppOpsManager.class)
+ .unsafeCheckOp(OPSTR_LEGACY_STORAGE, uid, packageName) == MODE_ALLOWED) {
+ return true;
+ }
+ // Check OPSTR_NO_ISOLATED_STORAGE app op.
+ return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
}
public static boolean checkPermissionReadAudio(@NonNull Context context, int pid, int uid,
diff --git a/tests/Android.bp b/tests/Android.bp
index 25dd84a..ed92db2 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -38,6 +38,7 @@
"androidx.test.rules",
"guava",
"mockito-target",
+ "modules-utils-build",
"truth-prebuilt",
],
diff --git a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
index e30ed92..40e1175 100644
--- a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
+++ b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
@@ -29,6 +29,7 @@
import org.junit.AfterClass;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -86,6 +87,7 @@
}
@Test
+ @Ignore("b/179675679")
public void testFromExternal() throws Exception {
final Context context = InstrumentationRegistry.getContext();
final PackageManager pm = context.getPackageManager();
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index e1312c2..d913307 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -777,6 +777,25 @@
}
}
+ @Test
+ public void testScan_audio_recording() throws Exception {
+ final File music = new File(mDir, "Recordings");
+ final File audio = new File(music, "audio.mp3");
+
+ music.mkdirs();
+ stage(R.raw.test_audio, audio);
+
+ mModern.scanFile(audio, REASON_UNKNOWN);
+
+ try (Cursor cursor = mIsolatedResolver
+ .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals(1, cursor.getInt(cursor.getColumnIndex(AudioColumns.IS_RECORDING)));
+ assertEquals(0, cursor.getInt(cursor.getColumnIndex(AudioColumns.IS_MUSIC)));
+ }
+ }
+
/**
* Verify a narrow exception where we allow an {@code mp4} video file on
* disk to be indexed as an {@code m4a} audio file.
diff --git a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
index e095168..3df318b 100644
--- a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
+++ b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
@@ -32,10 +32,10 @@
import android.media.ApplicationMediaCapabilities;
import android.media.MediaFormat;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
+import android.os.SystemProperties;
import android.provider.MediaStore;
import androidx.test.runner.AndroidJUnit4;
@@ -85,7 +85,7 @@
@Before
public void setUp() throws Exception {
// TODO(b/171789917): Cuttlefish doesn't support transcoding yet
- Assume.assumeFalse(Build.MODEL.contains("Cuttlefish"));
+ Assume.assumeFalse(SystemProperties.get("ro.product.vendor.model").contains("Cuttlefish"));
TranscodeTestUtils.pollForExternalStorageState();
TranscodeTestUtils.grantPermission(getContext().getPackageName(),
@@ -737,4 +737,94 @@
modernFile.delete();
}
}
+
+ /**
+ * Tests that we transcode an HEVC file when a modern app passes the mediaCapabilitiesUid of a
+ * legacy app that cannot handle an HEVC file.
+ */
+ @Test
+ public void testOriginalCallingUid_modernAppPassLegacyAppUid()
+ throws Exception {
+ File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+ ParcelFileDescriptor pfdModernApp = null;
+ ParcelFileDescriptor pfdModernAppPassingLegacyUid = null;
+ try {
+ installAppWithStoragePermissions(TEST_APP_SLOW_MOTION);
+ Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+ // pfdModernApp is for original content (without transcoding) since this is a modern
+ // app.
+ pfdModernApp = open(modernFile, false);
+
+ // pfdModernAppPassingLegacyUid is for transcoded content since this modern app is
+ // passing the UID of a legacy app capable of handling HEVC files.
+ Bundle bundle = new Bundle();
+ bundle.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID,
+ getContext().getPackageManager().getPackageUid(
+ TEST_APP_SLOW_MOTION.getPackageName(), 0));
+ pfdModernAppPassingLegacyUid = open(uri, false, bundle);
+
+ assertTranscode(pfdModernApp, false);
+ assertTranscode(pfdModernAppPassingLegacyUid, true);
+
+ // pfdModernApp and pfdModernAppPassingLegacyUid should be different.
+ assertFileContent(modernFile, modernFile, pfdModernApp, pfdModernAppPassingLegacyUid,
+ false);
+ } finally {
+ if (pfdModernApp != null) {
+ pfdModernApp.close();
+ }
+
+ if (pfdModernAppPassingLegacyUid != null) {
+ pfdModernAppPassingLegacyUid.close();
+ }
+ modernFile.delete();
+ uninstallApp(TEST_APP_SLOW_MOTION);
+ }
+ }
+
+ /**
+ * Tests that we don't transcode an HEVC file when a legacy app passes the mediaCapabilitiesUid
+ * of a modern app that can handle an HEVC file.
+ */
+ @Test
+ public void testOriginalCallingUid_legacyAppPassModernAppUid()
+ throws Exception {
+ File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+ ParcelFileDescriptor pfdLegacyApp = null;
+ ParcelFileDescriptor pfdLegacyAppPassingModernUid = null;
+ try {
+ installAppWithStoragePermissions(TEST_APP_HEVC);
+ Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+ // pfdLegacyApp is for transcoded content since this is a legacy app.
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+ pfdLegacyApp = open(modernFile, false);
+
+ // pfdLegacyAppPassingModernUid is for original content (without transcoding) since this
+ // legacy app is passing the UID of a modern app capable of handling HEVC files.
+ Bundle bundle = new Bundle();
+ bundle.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID,
+ getContext().getPackageManager().getPackageUid(TEST_APP_HEVC.getPackageName(),
+ 0));
+ pfdLegacyAppPassingModernUid = open(uri, false, bundle);
+
+ assertTranscode(pfdLegacyApp, true);
+ assertTranscode(pfdLegacyAppPassingModernUid, false);
+
+ // pfdLegacyApp and pfdLegacyAppPassingModernUid should be different.
+ assertFileContent(modernFile, modernFile, pfdLegacyApp, pfdLegacyAppPassingModernUid,
+ false);
+ } finally {
+ if (pfdLegacyApp != null) {
+ pfdLegacyApp.close();
+ }
+
+ if (pfdLegacyAppPassingModernUid != null) {
+ pfdLegacyAppPassingModernUid.close();
+ }
+ modernFile.delete();
+ uninstallApp(TEST_APP_HEVC);
+ }
+ }
}