diff options
author | 2025-03-13 12:03:31 -0700 | |
---|---|---|
committer | 2025-03-13 12:03:31 -0700 | |
commit | 69c71c18f7143fffc0559bd46491876d34245bd7 (patch) | |
tree | bb112688f5d18def0530caa68ab85df7c545fa02 | |
parent | b7f549960eff6450f38680215e453cf7368278dc (diff) | |
parent | 9a831d5492fb663b95b49693de14287988ccad30 (diff) |
Merge "Add API to bulk update oem_metadata" into main
13 files changed, 350 insertions, 15 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e4249ce1c..a19e718b5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -54,6 +54,10 @@ <uses-permission android:name="com.android.providers.media.permission.ACCESS_OEM_METADATA" /> + <!-- Permission required to update OEM metadata. Declared by us --> + <uses-permission + android:name="com.android.providers.media.permission.UPDATE_OEM_METADATA" /> + <!-- Permission required to bind to OemMetadataService --> <uses-permission android:name="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE" /> @@ -74,6 +78,9 @@ <permission android:name="com.android.providers.media.permission.ACCESS_OEM_METADATA" android:protectionLevel="signature|privileged" /> + <permission android:name="com.android.providers.media.permission.UPDATE_OEM_METADATA" + android:protectionLevel="signature|privileged" /> + <permission android:name="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE" android:protectionLevel="signature"/> diff --git a/apex/framework/api/system-current.txt b/apex/framework/api/system-current.txt index 1d11267f4..f002605a1 100644 --- a/apex/framework/api/system-current.txt +++ b/apex/framework/api/system-current.txt @@ -64,6 +64,7 @@ package android.provider { } public final class MediaStore { + method @FlaggedApi("com.android.providers.media.flags.enable_oem_metadata_update") public static void bulkUpdateOemMetadataInNextScan(@NonNull android.content.Context); method @NonNull public static android.net.Uri rewriteToLegacy(@NonNull android.net.Uri); method @NonNull @WorkerThread public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File); method @WorkerThread public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String); @@ -72,6 +73,7 @@ package android.provider { field public static final String AUTHORITY_LEGACY = "media_legacy"; field @NonNull public static final android.net.Uri AUTHORITY_LEGACY_URI; field public static final String QUERY_ARG_DEFER_SCAN = "android:query-arg-defer-scan"; + field @FlaggedApi("com.android.providers.media.flags.enable_oem_metadata_update") public static final String UPDATE_OEM_METADATA_PERMISSION = "com.android.providers.media.permission.UPDATE_OEM_METADATA"; } @FlaggedApi("com.android.providers.media.flags.enable_oem_metadata") public abstract class OemMetadataService extends android.app.Service { diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java index c9ee81196..1e64cbd4e 100644 --- a/apex/framework/java/android/provider/MediaStore.java +++ b/apex/framework/java/android/provider/MediaStore.java @@ -304,6 +304,9 @@ public final class MediaStore { "revoke_all_media_grants_for_package"; /** @hide */ + public static final String BULK_UPDATE_OEM_METADATA_CALL = "bulk_update_oem_metadata"; + + /** @hide */ public static final String OPEN_FILE_CALL = "open_file_call"; @@ -1342,6 +1345,16 @@ public final class MediaStore { public static final String ACCESS_OEM_METADATA_PERMISSION = "com.android.providers.media.permission.ACCESS_OEM_METADATA"; + /** + * Permission that grants ability to trigger update of {@link MediaColumns#OEM_METADATA}. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA_UPDATE) + @SystemApi + public static final String UPDATE_OEM_METADATA_PERMISSION = + "com.android.providers.media.permission.UPDATE_OEM_METADATA"; + /** @hide */ @IntDef(flag = true, prefix = { "MATCH_" }, value = { MATCH_DEFAULT, @@ -5734,4 +5747,26 @@ public final class MediaStore { throw e.rethrowAsRuntimeException(); } } + + /** + * Allows bulk update of {@link MediaColumns#OEM_METADATA} column in next scan. + * Requires calling package to hold {@link UPDATE_OEM_METADATA_PERMISSION} permission. Updates + * {@link MediaColumns#OEM_METADATA} to NULL for OEM supported media files and re-fetch + * the latest values in the next scan. + * Caller can enforce file/volume scan after this to update MediaStore with the latest OEM + * metadata. If not done, next scan by MediaStore will fetch and update the latest data. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA_UPDATE) + @SystemApi + public static void bulkUpdateOemMetadataInNextScan(@NonNull Context context) { + final ContentResolver resolver = context.getContentResolver(); + try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { + final Bundle extras = new Bundle(); + client.call(BULK_UPDATE_OEM_METADATA_CALL, /* arg= */ null, /* extras= */ extras); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } } diff --git a/mediaprovider_flags.aconfig b/mediaprovider_flags.aconfig index 7a6c28299..4b7871002 100644 --- a/mediaprovider_flags.aconfig +++ b/mediaprovider_flags.aconfig @@ -282,3 +282,12 @@ flag { is_fixed_read_only: true bug: "376910932" } + +flag { + name: "enable_oem_metadata_update" + is_exported: true + namespace: "mediaprovider" + description: "Controls ability to update oem_metadata column" + bug: "352528913" + is_fixed_read_only: true +} diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java index b85cdc881..8074656bd 100644 --- a/src/com/android/providers/media/LocalCallingIdentity.java +++ b/src/com/android/providers/media/LocalCallingIdentity.java @@ -35,6 +35,7 @@ import static com.android.providers.media.util.PermissionUtils.checkPermissionRe import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVisualUserSelected; import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf; import static com.android.providers.media.util.PermissionUtils.checkPermissionShell; +import static com.android.providers.media.util.PermissionUtils.checkPermissionUpdateOemMetadata; import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteAudio; import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages; import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage; @@ -353,6 +354,7 @@ public class LocalCallingIdentity { public static final int PERMISSION_QUERY_ALL_PACKAGES = 1 << 28; public static final int PERMISSION_ACCESS_MEDIA_OWNER_PACKAGE_NAME = 1 << 29; public static final int PERMISSION_ACCESS_OEM_METADATA = 1 << 30; + public static final int PERMISSION_UPDATE_OEM_METADATA = 1 << 31; private volatile int hasPermission; private volatile int hasPermissionResolved; @@ -452,6 +454,9 @@ public class LocalCallingIdentity { case PERMISSION_ACCESS_OEM_METADATA: return checkPermissionAccessOemMetadata(context, pid, uid, getPackageName(), attributionTag); + case PERMISSION_UPDATE_OEM_METADATA: + return checkPermissionUpdateOemMetadata(context, pid, uid, getPackageName(), + attributionTag); default: return false; } @@ -748,6 +753,13 @@ public class LocalCallingIdentity { } /** + * Returns {@code true} if this package has permission to update oem_metadata of any media. + */ + public boolean checkCallingPermissionToUpdateOemMetadata() { + return hasPermission(PERMISSION_UPDATE_OEM_METADATA); + } + + /** * Returns {@code true} if this package is a legacy app and has read permission */ public boolean isCallingPackageLegacyRead() { diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java index 51efa9210..5336b089f 100644 --- a/src/com/android/providers/media/MediaProvider.java +++ b/src/com/android/providers/media/MediaProvider.java @@ -34,6 +34,7 @@ import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTE import static android.provider.MediaStore.EXTRA_IS_STABLE_URIS_ENABLED; import static android.provider.MediaStore.EXTRA_OPEN_ASSET_FILE_REQUEST; import static android.provider.MediaStore.EXTRA_OPEN_FILE_REQUEST; +import static android.provider.MediaStore.EXTRA_URI_LIST; import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE; import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE; import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO; @@ -5849,6 +5850,12 @@ public class MediaProvider extends ContentProvider { } } + + // Enforce oem_metadata permission if caller is not MediaProvider + if (Flags.enableOemMetadataUpdate() && initialValues.containsKey(OEM_METADATA)) { + enforcePermissionCheckForOemMetadataUpdate(); + } + long rowId = -1; Uri newUri = null; @@ -7306,11 +7313,80 @@ public class MediaProvider extends ContentProvider { removeRecoveryData(); return new Bundle(); } + case MediaStore.BULK_UPDATE_OEM_METADATA_CALL: { + callForBulkUpdateOemMetadataColumn(); + return new Bundle(); + } default: throw new UnsupportedOperationException("Unsupported call: " + method); } } + private void callForBulkUpdateOemMetadataColumn() { + if (!Flags.enableOemMetadataUpdate()) { + return; + } + + enforcePermissionCheckForOemMetadataUpdate(); + Set<String> oemSupportedMimeTypes = mMediaScanner.getOemSupportedMimeTypes(); + if (oemSupportedMimeTypes == null || oemSupportedMimeTypes.isEmpty()) { + // Nothing to update + return; + } + + // Get media types to update rows based on media type + Set<Integer> mediaTypesToBeUpdated = new HashSet<>(); + for (String mimeType : oemSupportedMimeTypes) { + // Convert to media type to avoid using like clause on mime types to protect against + // SQL injection + mediaTypesToBeUpdated.add(MimeUtils.resolveMediaType(mimeType)); + } + + if (mediaTypesToBeUpdated.isEmpty()) { + // For invalid mime types, do not bother + return; + } + + final LocalCallingIdentity token = clearLocalCallingIdentity(); + try { + ContentValues values = new ContentValues(); + values.putNull(OEM_METADATA); + // Mark _modifier as _MODIFIER_CR to allow metadata update on next scan. This + // is explicitly required when calling update with MediaProvider identity + values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR); + Bundle extras = new Bundle(); + extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, + appendMediaTypeClause(mediaTypesToBeUpdated)); + Log.v(TAG, "Trigger bulk update of OEM metadata"); + update(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), values, extras); + } finally { + restoreLocalCallingIdentity(token); + } + } + + private String appendMediaTypeClause(Set<Integer> mediaTypesToBeUpdated) { + List<String> whereMediaTypesCondition = new ArrayList<String>(); + for (Integer mediaType : mediaTypesToBeUpdated) { + whereMediaTypesCondition.add( + String.format(Locale.ROOT, "%s=%d", MEDIA_TYPE, mediaType)); + } + + StringBuilder sb = new StringBuilder(); + sb.append("("); + sb.append(TextUtils.join(" OR ", whereMediaTypesCondition)); + sb.append(")"); + return sb.toString(); + } + + @VisibleForTesting + protected void enforcePermissionCheckForOemMetadataUpdate() { + if (!isCallingPackageSelf() + && !mCallingIdentity.get().checkCallingPermissionToUpdateOemMetadata()) { + throw new SecurityException( + "Calling package does not have permission to update OEM metadata"); + } + } + @Nullable private Bundle getResultForRevokeReadGrantForPackage(Bundle extras) { final int caller = Binder.getCallingUid(); @@ -7353,7 +7429,7 @@ public class MediaProvider extends ContentProvider { userId = uidToUserId(packageUid); // Uris are not a requirement for revoke all call if (!isCallForRevokeAll) { - uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); + uris = extras.getParcelableArrayList(EXTRA_URI_LIST); } } else { // All other callers are unauthorized. @@ -7630,14 +7706,14 @@ public class MediaProvider extends ContentProvider { @NotNull private Bundle getResultForGetRedactedMediaUriList(Bundle extras) { - final List<Uri> uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); + final List<Uri> uris = extras.getParcelableArrayList(EXTRA_URI_LIST); // NOTE: It is ok to update the DB and return a redacted URI for the cases when // the user code only has read access, hence we don't check for write permission. enforceCallingPermission(uris, false); final LocalCallingIdentity token = clearLocalCallingIdentity(); try { final Bundle res = new Bundle(); - res.putParcelableArrayList(MediaStore.EXTRA_URI_LIST, + res.putParcelableArrayList(EXTRA_URI_LIST, (ArrayList<? extends Parcelable>) getRedactedUri(uris)); return res; } finally { @@ -7668,12 +7744,12 @@ public class MediaProvider extends ContentProvider { } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) { // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST // and EXTRA_UID. - if (!extras.containsKey(MediaStore.EXTRA_URI_LIST) + if (!extras.containsKey(EXTRA_URI_LIST) && !extras.containsKey(Intent.EXTRA_UID)) { throw new IllegalArgumentException( "Missing required extras arguments: EXTRA_URI_LIST or" + " EXTRA_UID"); } - uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); + uris = extras.getParcelableArrayList(EXTRA_URI_LIST); final PackageManager pm = getContext().getPackageManager(); final int packageUid = extras.getInt(Intent.EXTRA_UID); final String[] packages = pm.getPackagesForUid(packageUid); @@ -8753,6 +8829,11 @@ public class MediaProvider extends ContentProvider { initialValues.remove(FileColumns.GENERATION_MODIFIED); } + // Enforce oem_metadata permission if caller is not MediaProvider + if (Flags.enableOemMetadataUpdate() && initialValues.containsKey(OEM_METADATA)) { + enforcePermissionCheckForOemMetadataUpdate(); + } + if (!isCallingPackageSelf()) { Trace.beginSection("MP.filter"); @@ -12058,6 +12139,8 @@ public class MediaProvider extends ContentProvider { sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE); sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); + + sMutableColumns.add(MediaStore.Files.FileColumns.OEM_METADATA); } /** diff --git a/src/com/android/providers/media/scan/LegacyMediaScanner.java b/src/com/android/providers/media/scan/LegacyMediaScanner.java index d73dda584..39bffe2f5 100644 --- a/src/com/android/providers/media/scan/LegacyMediaScanner.java +++ b/src/com/android/providers/media/scan/LegacyMediaScanner.java @@ -22,6 +22,7 @@ import android.net.Uri; import com.android.providers.media.MediaVolume; import java.io.File; +import java.util.Set; @Deprecated public class LegacyMediaScanner implements MediaScanner { @@ -60,4 +61,9 @@ public class LegacyMediaScanner implements MediaScanner { public void onDirectoryDirty(File file) { throw new UnsupportedOperationException(); } + + @Override + public Set<String> getOemSupportedMimeTypes() { + throw new UnsupportedOperationException(); + } } diff --git a/src/com/android/providers/media/scan/MediaScanner.java b/src/com/android/providers/media/scan/MediaScanner.java index 8999542bf..2ce537409 100644 --- a/src/com/android/providers/media/scan/MediaScanner.java +++ b/src/com/android/providers/media/scan/MediaScanner.java @@ -33,6 +33,7 @@ import com.android.providers.media.MediaVolume; import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Set; public interface MediaScanner { int REASON_UNKNOWN = MEDIA_PROVIDER_SCAN_OCCURRED__REASON__UNKNOWN; @@ -62,4 +63,9 @@ public interface MediaScanner { void onIdleScanStopped(); void onDirectoryDirty(@NonNull File file); + + /** + * Returns OEM supported mime types for OEM metadata. + */ + Set<String> getOemSupportedMimeTypes(); } diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java index 3b9f3c4bb..da6b30b5f 100644 --- a/src/com/android/providers/media/scan/ModernMediaScanner.java +++ b/src/com/android/providers/media/scan/ModernMediaScanner.java @@ -287,7 +287,8 @@ public class ModernMediaScanner implements MediaScanner { } } - private Set<String> getOemSupportedMimeTypes() { + @Override + public Set<String> getOemSupportedMimeTypes() { try { // Return if no package implements OemMetadataService if (!mDefaultOemMetadataServicePackage.isPresent()) { diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java index 97f14aebb..2458785e2 100644 --- a/src/com/android/providers/media/util/PermissionUtils.java +++ b/src/com/android/providers/media/util/PermissionUtils.java @@ -360,7 +360,7 @@ public class PermissionUtils { /** * Check if the given package has been granted the - * android.provider.MediaStore#ACCESS_OEM_METADATA_PERMISSION permission. + * {@link android.provider.MediaStore#ACCESS_OEM_METADATA_PERMISSION} permission. */ public static boolean checkPermissionAccessOemMetadata(@NonNull Context context, int pid, int uid, @NonNull String packageName, @Nullable String attributionTag) { @@ -368,6 +368,16 @@ public class PermissionUtils { pid, uid, packageName, attributionTag, null); } + /** + * Check if the given package has been granted the + * {@link android.provider.MediaStore#UPDATE_OEM_METADATA_PERMISSION} permission. + */ + public static boolean checkPermissionUpdateOemMetadata(@NonNull Context context, + int pid, int uid, @NonNull String packageName, @Nullable String attributionTag) { + return checkPermissionForPreflight(context, MediaStore.UPDATE_OEM_METADATA_PERMISSION, + pid, uid, packageName); + } + public static boolean checkPermissionInstallPackages(@NonNull Context context, int pid, int uid, @NonNull String packageName, @Nullable String attributionTag) { return checkPermissionForDataDelivery(context, INSTALL_PACKAGES, pid, diff --git a/tests/src/com/android/providers/media/IsolatedContext.java b/tests/src/com/android/providers/media/IsolatedContext.java index fa9a103ba..1b0f1f31e 100644 --- a/tests/src/com/android/providers/media/IsolatedContext.java +++ b/tests/src/com/android/providers/media/IsolatedContext.java @@ -152,6 +152,11 @@ public class IsolatedContext extends ContextWrapper { protected boolean shouldCheckForMaliciousActivity() { return Flags.enableMaliciousAppDetector(); } + + @Override + protected void enforcePermissionCheckForOemMetadataUpdate(){ + + } }; } diff --git a/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java b/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java index 774635be4..d8001c6c4 100644 --- a/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java +++ b/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java @@ -26,6 +26,7 @@ import android.Manifest; import android.app.Instrumentation; import android.content.ComponentName; import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; @@ -63,6 +64,7 @@ import org.junit.runner.RunWith; import java.io.File; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -71,7 +73,6 @@ import java.util.concurrent.TimeoutException; import java.util.function.Supplier; @RunWith(AndroidJUnit4.class) -@EnableFlags(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) public class OemMetadataServiceTest { @@ -105,6 +106,7 @@ public class OemMetadataServiceTest { } @Test + @EnableFlags(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA) public void testGetSupportedMimeTypes() throws Exception { bindService(); assertNotNull(mOemMetadataServiceWrapper); @@ -116,6 +118,7 @@ public class OemMetadataServiceTest { } @Test + @EnableFlags(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA) public void testGetOemCustomData() throws Exception { bindService(); assertNotNull(mOemMetadataServiceWrapper); @@ -141,6 +144,7 @@ public class OemMetadataServiceTest { } @Test + @EnableFlags(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA) public void testScanOfOemMetadataAndFilterOnReadWithoutPermission() throws Exception { IsolatedContext isolatedContext = new IsolatedContext(mContext, "modern", /* asFuseThread */ false); @@ -181,6 +185,130 @@ public class OemMetadataServiceTest { } @Test + @EnableFlags({com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA, + com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA_UPDATE}) + public void testTriggerOemMetadataUpdateWithPermission() throws Exception { + IsolatedContext isolatedContext = new IsolatedContext(mContext, "modern", + /* asFuseThread */ false); + ModernMediaScanner modernMediaScanner = new ModernMediaScanner(isolatedContext, + new TestConfigStore()); + final File downloads = new File(Environment.getExternalStorageDirectory(), + Environment.DIRECTORY_DOWNLOADS); + final File audioFile = new File(downloads, "audio.mp3"); + try { + stage(R.raw.test_audio, audioFile); + Uri uri = modernMediaScanner.scanFile(audioFile, MediaScanner.REASON_UNKNOWN); + DatabaseHelper databaseHelper = isolatedContext.getExternalDatabase(); + // Direct query on DB returns stored value of oem_metadata + try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query( + "files", new String[]{FileColumns.OEM_METADATA, FileColumns._MODIFIER}, + "_id=?", new String[]{String.valueOf(ContentUris.parseId(uri))}, + null, null, null))) { + assertThat(c.getCount()).isEqualTo(1); + c.moveToNext(); + byte[] oemData = c.getBlob(0); + int modifier = c.getInt(1); + assertThat(oemData).isNotNull(); + Map<String, String> map = convertStringToOemMetadataMap(new String(oemData)); + assertThat(map.keySet()).containsExactly("a", "b", "c", "d", "e"); + assertThat(modifier).isEqualTo(FileColumns._MODIFIER_MEDIA_SCAN); + } + + ContentValues contentValues = new ContentValues(); + Map<String, String> updatedData = Map.of("a1", "b1", "a2", "b2"); + contentValues.put(FileColumns.OEM_METADATA, updatedData.toString()); + isolatedContext.getContentResolver().update(uri, contentValues, null); + + try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query( + "files", new String[]{FileColumns.OEM_METADATA, FileColumns._MODIFIER}, + "_id=?", new String[]{String.valueOf(ContentUris.parseId(uri))}, + null, null, null))) { + assertThat(c.getCount()).isEqualTo(1); + c.moveToNext(); + byte[] oemData = c.getBlob(0); + int modifier = c.getInt(1); + assertThat(modifier).isEqualTo(FileColumns._MODIFIER_MEDIA_SCAN); + assertThat(oemData).isNotNull(); + Map<String, String> map = convertStringToOemMetadataMap(new String(oemData)); + assertThat(map.keySet()).containsExactly("a1", "a2"); + } + } finally { + audioFile.delete(); + } + } + + @Test + @EnableFlags({com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA, + com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA_UPDATE}) + public void testTriggerBulkUpdateOemMetadataInNextScan() throws Exception { + IsolatedContext isolatedContext = new IsolatedContext(mContext, "modern", + /* asFuseThread */ false); + ModernMediaScanner modernMediaScanner = new ModernMediaScanner(isolatedContext, + new TestConfigStore()); + final File downloads = new File(Environment.getExternalStorageDirectory(), + Environment.DIRECTORY_DOWNLOADS); + final File audioFile = new File(downloads, "audio.mp3"); + try { + stage(R.raw.test_audio, audioFile); + + Uri uri = modernMediaScanner.scanFile(audioFile, MediaScanner.REASON_UNKNOWN); + + DatabaseHelper databaseHelper = isolatedContext.getExternalDatabase(); + // Direct query on DB returns stored value of oem_metadata + try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query( + "files", new String[]{FileColumns.OEM_METADATA, FileColumns._MODIFIER}, + "_id=?", new String[]{String.valueOf(ContentUris.parseId(uri))}, + null, null, null))) { + assertThat(c.getCount()).isEqualTo(1); + c.moveToNext(); + byte[] oemData = c.getBlob(0); + int modifier = c.getInt(1); + assertThat(oemData).isNotNull(); + Map<String, String> map = convertStringToOemMetadataMap(new String(oemData)); + assertThat(map.keySet()).containsExactly("a", "b", "c", "d", "e"); + assertThat(modifier).isEqualTo(FileColumns._MODIFIER_MEDIA_SCAN); + } + + // Change service behavior to verify updated results. Add new key "f". + TestOemMetadataService.updateOemMetadataServiceData(); + // OEM metadata should be allowed to update to null and modifier + // should now be set to _MODIFIER_CR as scan has not happened yet + MediaStore.bulkUpdateOemMetadataInNextScan(isolatedContext); + try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query( + "files", new String[]{FileColumns.OEM_METADATA, FileColumns._MODIFIER}, + "_id=?", new String[]{String.valueOf(ContentUris.parseId(uri))}, + null, null, null))) { + assertThat(c.getCount()).isEqualTo(1); + c.moveToNext(); + byte[] oemData = c.getBlob(0); + int modifier = c.getInt(1); + assertThat(oemData).isNull(); + assertThat(modifier).isEqualTo(FileColumns._MODIFIER_CR); + } + + // Trigger scan to allow OEM metadata update + MediaStore.scanFile(isolatedContext.getContentResolver(), audioFile); + + try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query( + "files", new String[]{FileColumns.OEM_METADATA, FileColumns._MODIFIER}, + "_id=?", new String[]{String.valueOf(ContentUris.parseId(uri))}, + null, null, null))) { + assertThat(c.getCount()).isEqualTo(1); + c.moveToNext(); + byte[] oemData = c.getBlob(0); + int modifier = c.getInt(1); + assertThat(modifier).isEqualTo(FileColumns._MODIFIER_MEDIA_SCAN); + assertThat(oemData).isNotNull(); + Map<String, String> map = convertStringToOemMetadataMap(new String(oemData)); + assertThat(map.keySet()).containsExactly("a", "b", "c", "d", "e", "f"); + } + } finally { + audioFile.delete(); + TestOemMetadataService.resetOemMetadataServiceData(); + } + } + + @Test public void testNoServiceBindingWithoutPermission() throws Exception { updateStateOfServiceWithPermission(PackageManager.COMPONENT_ENABLED_STATE_DISABLED); IsolatedContext isolatedContext = new IsolatedContext(mContext, "modern", @@ -272,4 +400,24 @@ public class OemMetadataServiceTest { mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); mServiceLatch.await(3, TimeUnit.SECONDS); } + + public static Map<String, String> convertStringToOemMetadataMap(String stringMapping) { + Map<String, String> map = new HashMap<>(); + if (stringMapping == null || stringMapping.isEmpty()) { + return map; + } + stringMapping = stringMapping.substring(1, stringMapping.length() - 1); + // Split into key-value pairs + String[] pairs = stringMapping.split(", "); + + for (String pair : pairs) { + String[] keyValue = pair.split("="); + String key = keyValue[0]; + String value = keyValue[1]; + if (key != null) { + map.put(key, value); + } + } + return map; + } } diff --git a/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java b/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java index cdc4f71ce..ae1dcf8a2 100644 --- a/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java +++ b/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java @@ -27,6 +27,15 @@ import java.util.Set; public class TestOemMetadataService extends OemMetadataService { + static Map<String, String> sOemMetadata = new HashMap<>(); + + static { + sOemMetadata.put("a", "1"); + sOemMetadata.put("b", "2"); + sOemMetadata.put("c", "3"); + sOemMetadata.put("d", "4"); + sOemMetadata.put("e", "5"); + } @Override public Set<String> onGetSupportedMimeTypes() { @@ -35,12 +44,14 @@ public class TestOemMetadataService extends OemMetadataService { @Override public Map<String, String> onGetOemCustomData(@NonNull ParcelFileDescriptor pfd) { - Map<String, String> oemMetadata = new HashMap<>(); - oemMetadata.put("a", "1"); - oemMetadata.put("b", "2"); - oemMetadata.put("c", "3"); - oemMetadata.put("d", "4"); - oemMetadata.put("e", "5"); - return oemMetadata; + return sOemMetadata; + } + + public static void updateOemMetadataServiceData() { + sOemMetadata.put("f", "6"); + } + + public static void resetOemMetadataServiceData() { + sOemMetadata.remove("f"); } } |