diff options
author | 2024-09-20 11:52:44 +0100 | |
---|---|---|
committer | 2024-10-24 13:43:06 +0100 | |
commit | 8885490578b0a91dba973c9f5b36ffef761a7e32 (patch) | |
tree | 94c17f6b3f2f1ade8cc2d7a2605f2b145f9e2b72 /apex | |
parent | ea4bf3dc76e434c78a9641db779e5d5535e0f189 (diff) |
Add CloudMediaProviderContract.Capabilities APIs and MEDIA_CATEGORY_TYPE_USER_ALBUMS.
Introduce the getCapabilities API in CloudMediaProvider to allow the
system to determine which optional APIs are available for a provider.
By default, the CloudMediaProvider will return the base implementation
set of features and newly added features will be optional off-by-default
forcing new implementations to declare their support for a capability by
overriding getCapabilities and explicitly enabling support.
Introduce MEDIA_CATEGORY_TYPE_USER_ALBUMS for allowing
CloudMediaProvider to provide albums as a MediaCategory rather than via
onQueryAlbums. This API will be unhidden at a future date.
Bug: b/316356081
Test: atest MediaProviderTests:CloudMediaProviderTest
Flag: com.android.providers.media.flags.enable_cloud_media_provider_capabilities
Change-Id: I2af93a72e7e03b64018b381bdb41b2fc00f3ecd1
Diffstat (limited to 'apex')
4 files changed, 356 insertions, 4 deletions
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt index 0290f7c16..c0dc1e3f9 100644 --- a/apex/framework/api/current.txt +++ b/apex/framework/api/current.txt @@ -10,6 +10,7 @@ package android.provider { method @NonNull public final String getType(@NonNull android.net.Uri); method @NonNull public final android.net.Uri insert(@NonNull android.net.Uri, @NonNull android.content.ContentValues); method @Nullable public android.provider.CloudMediaProvider.CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull android.os.Bundle, @NonNull android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback); + method @FlaggedApi("com.android.providers.media.flags.enable_cloud_media_provider_capabilities") @NonNull public android.provider.CloudMediaProviderContract.Capabilities onGetCapabilities(); method @NonNull public abstract android.os.Bundle onGetMediaCollectionInfo(@NonNull android.os.Bundle); method @NonNull public abstract android.os.ParcelFileDescriptor onOpenMedia(@NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException; method @NonNull public abstract android.content.res.AssetFileDescriptor onOpenPreview(@NonNull String, @NonNull android.graphics.Point, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException; @@ -87,6 +88,21 @@ package android.provider { field public static final String MEDIA_COVER_ID = "album_media_cover_id"; } + @FlaggedApi("com.android.providers.media.flags.enable_cloud_media_provider_capabilities") public static final class CloudMediaProviderContract.Capabilities implements android.os.Parcelable { + method public int describeContents(); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public boolean isMediaCollectionsEnabled(); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public boolean isSearchEnabled(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.provider.CloudMediaProviderContract.Capabilities> CREATOR; + } + + @FlaggedApi("com.android.providers.media.flags.enable_cloud_media_provider_capabilities") public static final class CloudMediaProviderContract.Capabilities.Builder { + ctor public CloudMediaProviderContract.Capabilities.Builder(); + method @NonNull public android.provider.CloudMediaProviderContract.Capabilities build(); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.provider.CloudMediaProviderContract.Capabilities.Builder setMediaCollectionsEnabled(boolean); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.provider.CloudMediaProviderContract.Capabilities.Builder setSearchEnabled(boolean); + } + @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final class CloudMediaProviderContract.MediaCategoryColumns { field public static final String DISPLAY_NAME = "display_name"; field public static final String ID = "id"; diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java index f9f8aace4..1d13426ad 100644 --- a/apex/framework/java/android/provider/CloudMediaProvider.java +++ b/apex/framework/java/android/provider/CloudMediaProvider.java @@ -22,6 +22,7 @@ import static android.provider.CloudMediaProviderContract.EXTRA_ERROR_MESSAGE; import static android.provider.CloudMediaProviderContract.EXTRA_FILE_DESCRIPTOR; import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED; import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB; +import static android.provider.CloudMediaProviderContract.EXTRA_PROVIDER_CAPABILITIES; import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER; import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED; import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK; @@ -32,6 +33,7 @@ import static android.provider.CloudMediaProviderContract.KEY_PREFIX_TEXT; import static android.provider.CloudMediaProviderContract.KEY_SEARCH_TEXT; import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER; import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER; +import static android.provider.CloudMediaProviderContract.METHOD_GET_CAPABILITIES; import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO; import static android.provider.CloudMediaProviderContract.URI_PATH_ALBUM; import static android.provider.CloudMediaProviderContract.URI_PATH_DELETED_MEDIA; @@ -184,6 +186,27 @@ public abstract class CloudMediaProvider extends ContentProvider { } /** + * Returns the {@link CloudMediaProviderContract.Capabilities} of this + * CloudMediaProvider. + * + * This object is used to determine which APIs can be safely invoked during + * runtime. + * + * If not overridden the default capabilities are used. + * + * IMPORTANT: This method is performance critical and should avoid long running + * or expensive operations. + * + * @see CloudMediaProviderContract.Capabilities + * + */ + @NonNull + @FlaggedApi(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES) + public CloudMediaProviderContract.Capabilities onGetCapabilities() { + return new CloudMediaProviderContract.Capabilities.Builder().build(); + } + + /** * Returns metadata about the media collection itself. * <p> * This is useful for the OS to determine if its cache of media items in the collection is @@ -694,7 +717,7 @@ public abstract class CloudMediaProvider extends ContentProvider { } private Bundle callUnchecked(@NonNull String method, @Nullable String arg, - @Nullable Bundle extras) + @Nullable Bundle extras) throws FileNotFoundException { if (extras == null) { extras = new Bundle(); @@ -704,12 +727,22 @@ public abstract class CloudMediaProvider extends ContentProvider { long startTime = System.currentTimeMillis(); result = onGetMediaCollectionInfo(extras); CmpApiVerifier.verifyApiResult(new CmpApiResult( - CmpApiVerifier.CloudMediaProviderApis.OnGetMediaCollectionInfo, result), + CmpApiVerifier.CloudMediaProviderApis.OnGetMediaCollectionInfo, result), System.currentTimeMillis() - startTime, mAuthority); } else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) { result = onCreateCloudMediaSurfaceController(extras); } else if (METHOD_GET_ASYNC_CONTENT_PROVIDER.equals(method)) { result = onGetAsyncContentProvider(); + } else if (Flags.enableCloudMediaProviderCapabilities() + && METHOD_GET_CAPABILITIES.equals(method)) { + long startTime = System.currentTimeMillis(); + + CloudMediaProviderContract.Capabilities capabilities = onGetCapabilities(); + result.putParcelable(EXTRA_PROVIDER_CAPABILITIES, capabilities); + + CmpApiVerifier.verifyApiResult(new CmpApiResult( + CmpApiVerifier.CloudMediaProviderApis.OnGetCapabilities, result), + System.currentTimeMillis() - startTime, mAuthority); } else { throw new UnsupportedOperationException("Method not supported " + method); } diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java index 4625503ca..0ac27a4fd 100644 --- a/apex/framework/java/android/provider/CloudMediaProviderContract.java +++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java @@ -20,11 +20,14 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.FlaggedApi; import android.annotation.IntDef; +import android.annotation.NonNull; import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import com.android.providers.media.flags.Flags; @@ -58,6 +61,235 @@ public final class CloudMediaProviderContract { public static final String MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION = "com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"; + /** + * Information about what capabilities a CloudMediaProvider can support. This + * will be used by the system to inform which APIs should be expected to return + * data. This object is returned from {@link CloudMediaProvider#onGetCapabilities}. + * + * This object enumerates which capabilities are provided by the + * CloudMediaProvider implementation that supplied this object. + * + * @see CloudMediaProvider#onGetCapabilities() + */ + @FlaggedApi(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES) + public static final class Capabilities implements Parcelable { + + private boolean mSearchEnabled; + private boolean mMediaCollectionsEnabled; + private boolean mAlbumsAsCategory; + + Capabilities(@NonNull Builder builder) { + this.mSearchEnabled = builder.mSearchEnabled; + this.mMediaCollectionsEnabled = builder.mMediaCollectionsEnabled; + this.mAlbumsAsCategory = builder.mAlbumsAsCategoryEnabled; + } + + + /** + * If the CloudMediaProvider supports Search functionality. + * + * In order for search to be enabled the CloudMediaProvider needs to + * implement the following APIs: + * + * @see CloudMediaProvider#onSearchMedia + * @see CloudMediaProvider#onQuerySearchSuggestions + * + * This capability is disabled by default. + * + * @return true if search is enabled for this CloudMediaProvider. + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public boolean isSearchEnabled() { + return mSearchEnabled; + } + + /** + * If the CloudMediaProvider supports MediaCollections. + * + * In order for MediaCollections to be enabled the CloudMediaProvider needs to + * implement the following APIs: + * + * @see CloudMediaProvider#onQueryMediaCategories + * @see CloudMediaProvider#onQueryMediaSets + * + * This capability is disabled by default. + * + * @return true if media collections are enabled for this CloudMediaProvider. + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public boolean isMediaCollectionsEnabled() { + return mMediaCollectionsEnabled; + } + + /** + * If the CloudMediaProvider will return user albums as a grouped category. + * + * When this capability is enabled, {@link CloudMediaProvider#onQueryAlbums} will + * no longer be called to sync the users albums, and it is expected that a + * category with the type {@link #MEDIA_CATEGORY_TYPE_USER_ALBUMS} will be + * provided in the {@link CloudMediaProvider#onQueryMediaCategories} for + * providing the user's custom albums. If no such category is returned, + * then there will be no data for user custom albums. + * + * NOTE: This capability requires the + * {@link Capabilities#isMediaCollectionsEnabled} capability to also be enabled + * for the CloudMediaProvider. If it is not, this Capability has no effect and + * will be ignored. + * + * @see CloudMediaProvider#onQueryMediaCategories + * @see #MEDIA_CATEGORY_TYPE_USER_ALBUMS + * + * This capability is disabled by default. + * + * @return true if albums will be returned as a MediaCategory. + * + * @hide + */ + public boolean isAlbumsAsCategoryEnabled() { + return mAlbumsAsCategory; + } + + /** + * Implemented for {@link Parcelable} + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Implemented for {@link Parcelable} + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mSearchEnabled); + dest.writeBoolean(mMediaCollectionsEnabled); + dest.writeBoolean(mAlbumsAsCategory); + } + + /** + * Implemented for {@link Parcelable} + */ + @NonNull + public static final Parcelable.Creator<Capabilities> CREATOR = + new Parcelable.Creator<Capabilities>() { + + @NonNull + @Override + public Capabilities createFromParcel(Parcel source) { + boolean searchEnabled = source.readBoolean(); + boolean mediaCollectionsEnabled = source.readBoolean(); + boolean mAlbumsAsCategoryEnabled = source.readBoolean(); + + Capabilities.Builder builder = new Capabilities.Builder(); + + if (Flags.cloudMediaProviderSearch()) { + builder + .setSearchEnabled(searchEnabled) + .setMediaCollectionsEnabled(mediaCollectionsEnabled) + .setAlbumsAsCategoryEnabled(mAlbumsAsCategoryEnabled); + } + + return builder.build(); + } + + @NonNull + @Override + public Capabilities[] newArray(int size) { + return new Capabilities[size]; + } + }; + + /** + * Builder for a {@link CloudMediaProviderContract.Capabilities} object. + * + * @see Capabilities + */ + @FlaggedApi(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES) + public static final class Builder { + + // Default values for each capability. These are used if not explicitly changed. + private boolean mSearchEnabled = false; + private boolean mMediaCollectionsEnabled = false; + private boolean mAlbumsAsCategoryEnabled = false; + + public Builder() { + } + + + /** + * The SearchEnabled capability informs that search related APIs are supported + * and can be invoked on this provider. + * + * @see CloudMediaProvider#onSearchMedia + * @see CloudMediaProvider#onQuerySearchSuggestions + * + * @param enabled true if this capability is supported, the default value is false. + */ + @NonNull + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public Builder setSearchEnabled(boolean enabled) { + mSearchEnabled = enabled; + return this; + } + + /** + * The MediaCollections capability informs that collection related APIs are + * supported and can be invoked on this provider. + * + * @see CloudMediaProvider#onQueryMediaCategories + * @see CloudMediaProvider#onQueryMediaSets + * + * @param enabled true if this capability is supported, the default value is false. + */ + @NonNull + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public Builder setMediaCollectionsEnabled(boolean enabled) { + mMediaCollectionsEnabled = enabled; + return this; + } + + /** + * If the CloudMediaProvider will return user albums as a grouped category. + * + * When this capability is enabled, {@link CloudMediaProvider#onQueryAlbums} will + * no longer be called to sync the users albums, and it is expected that a + * category with the type {@link #MEDIA_CATEGORY_TYPE_USER_ALBUMS} will be + * provided in the {@link CloudMediaProvider#onQueryMediaCategories} for + * providing the user's custom albums. If no such category is returned, + * then there will be no data for user custom albums. + * + * NOTE: This capability requires the + * {@link Capabilities#isMediaCollectionsEnabled} capability to also be enabled + * for the CloudMediaProvider. If it is not, this Capability has no effect and + * will be ignored. + * + * @see CloudMediaProvider#onQueryMediaCategories + * @see #MEDIA_CATEGORY_TYPE_USER_ALBUMS + * + * @param enabled true if this capability is supported, the default value is false. + * + * @hide + */ + @NonNull + public Builder setAlbumsAsCategoryEnabled(boolean enabled) { + mAlbumsAsCategoryEnabled = enabled; + return this; + } + + /** + * Create a new {@link CloudMediaProviderContract.Capabilities} object with the + * current builder's Capabilities. + */ + @NonNull + public Capabilities build() { + return new Capabilities(this); + } + + } + + } + /** Constants related to a media item, including {@link Cursor} column names */ public static final class MediaColumns { private MediaColumns() {} @@ -710,6 +942,21 @@ public final class CloudMediaProviderContract { "android:getAsyncContentProvider"; /** + * Constant used to execute {@link CloudMediaProvider#onGetCapabilities()} via + * {@link android.content.ContentProvider#call}. + * + */ + static final String METHOD_GET_CAPABILITIES = "android:getCapabilities"; + + /** + * Constant used to get/set {@link Capabilities} in {@link Bundle}. + * + */ + static final String EXTRA_PROVIDER_CAPABILITIES = + "android.provider.extra.PROVIDER_CAPABILITIES"; + + + /** * Constant used to get/set {@link IAsyncContentProvider} in {@link Bundle}. * * {@hide} @@ -985,7 +1232,9 @@ public final class CloudMediaProviderContract { /** * The type of the media category. * This must contain one of the values from the supported media category types. - * Currently supported types are: {@link #MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS} + * Currently supported types are: + * {@link #MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS} + * {@link #MEDIA_CATEGORY_TYPE_USER_ALBUMS} * * Type: INTEGER */ @@ -1061,13 +1310,22 @@ public final class CloudMediaProviderContract { public static final int MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS = 1; /** + * Represents media category related to a user's custom albums. + * @see MediaCategoryColumns#MEDIA_CATEGORY_TYPE + * Type: INTEGER + * + * @hide + */ + public static final int MEDIA_CATEGORY_TYPE_USER_ALBUMS = 2; + + /** * Defines the types of media categories available and supported in photo picker. * All MediaCategories returned must be of any type from the fields available in this class. * * @see MediaCategoryColumns#MEDIA_CATEGORY_TYPE * @hide */ - @IntDef(value = {MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS}) + @IntDef(value = {MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS, MEDIA_CATEGORY_TYPE_USER_ALBUMS}) @Retention(SOURCE) public @interface MediaCategoryTypes {} diff --git a/apex/framework/java/android/provider/CmpApiVerifier.java b/apex/framework/java/android/provider/CmpApiVerifier.java index 4977d45be..fab82a651 100644 --- a/apex/framework/java/android/provider/CmpApiVerifier.java +++ b/apex/framework/java/android/provider/CmpApiVerifier.java @@ -80,6 +80,10 @@ final class CmpApiVerifier { CMP_API_TO_THRESHOLD_MAP.get(result.getApi()), errors); switch (result.getApi()) { + case CloudMediaProviderApis.OnGetCapabilities: { + verifyOnGetCapabilities(result.getBundle(), verificationResult, errors); + break; + } case CloudMediaProviderApis.OnGetMediaCollectionInfo: { verifyOnGetMediaCollectionInfo(result.getBundle(), verificationResult, errors); @@ -144,6 +148,44 @@ final class CmpApiVerifier { } /** + * Verifies the {@link CloudMediaProvider#onGetCapabilities()} API. + * + * Verifies the Capabilities object returned is non-null. + */ + static void verifyOnGetCapabilities( + Bundle outputBundle, List<String> verificationResult, List<String> errors) { + + // Only Verify if the flag for capabilities is on. + if (Flags.enableCloudMediaProviderCapabilities()) { + + if (outputBundle != null + && outputBundle.containsKey( + CloudMediaProviderContract.EXTRA_PROVIDER_CAPABILITIES)) { + + verificationResult.add("Capabilities is present."); + + CloudMediaProviderContract.Capabilities capabilities = outputBundle + .getParcelable(CloudMediaProviderContract.EXTRA_PROVIDER_CAPABILITIES); + + // Verify CMP search capabilities if the search flag is on. + if (Flags.cloudMediaProviderSearch()) { + if (capabilities.isAlbumsAsCategoryEnabled() + && !capabilities.isMediaCollectionsEnabled()) { + errors.add(createIsNotValidLog("Declared capabilities are invalid. " + + "AlbumsAsCategory capability can only be enabled when " + + "MediaCollections is enabled.")); + } else { + verificationResult.add("Declared Capabilities are valid."); + } + + } + } else { + errors.add(createIsNullLog("Capabilities were not returned by OnGetCapabilities")); + } + } + } + + /** * Verifies OnGetMediaCollectionInfo API by performing and logging the following checks: * * <ul> @@ -520,6 +562,7 @@ final class CmpApiVerifier { } @StringDef({ + CloudMediaProviderApis.OnGetCapabilities, CloudMediaProviderApis.OnGetMediaCollectionInfo, CloudMediaProviderApis.OnQueryMedia, CloudMediaProviderApis.OnQueryDeletedMedia, @@ -534,6 +577,7 @@ final class CmpApiVerifier { }) @Retention(RetentionPolicy.SOURCE) @interface CloudMediaProviderApis { + String OnGetCapabilities = "OnGetCapabilities"; String OnGetMediaCollectionInfo = "onGetMediaCollectionInfo"; String OnQueryMedia = "onQueryMedia"; String OnQueryDeletedMedia = "onQueryDeletedMedia"; @@ -548,6 +592,7 @@ final class CmpApiVerifier { } private static final Map<String, Long> CMP_API_TO_THRESHOLD_MAP = new HashMap<>(Map.ofEntries( + Map.entry(CloudMediaProviderApis.OnGetCapabilities, 200L), Map.entry(CloudMediaProviderApis.OnGetMediaCollectionInfo, 200L), Map.entry(CloudMediaProviderApis.OnQueryMedia, 500L), Map.entry(CloudMediaProviderApis.OnQueryDeletedMedia, 500L), |