diff options
author | 2024-05-29 13:27:29 +0000 | |
---|---|---|
committer | 2024-10-04 03:51:23 +0000 | |
commit | abafc3b678b59bd89a531aa0c6a52342ae7989b0 (patch) | |
tree | 6b6a2a7654558e736123f17fbdf9bb5601eae556 /apex | |
parent | 77e89cb98b0da8909c2393d0fbf305b72ae48599 (diff) |
Search and Derived Collection Apis for CloudMediaProvider.
docs: go/cloud-search-apis , go/search-photopicker
Test: Build
Bug: b/316356081
Test: PickerSearchProviderClientTest.java
Flag: com.android.providers.media.flags.cloud_media_provider_search
API-Coverage-Bug: 358845745
Change-Id: I95a85eb432599ee299f0c335fd085cd8cabf13e4
Diffstat (limited to 'apex')
-rw-r--r-- | apex/framework/api/current.txt | 38 | ||||
-rw-r--r-- | apex/framework/java/android/provider/CloudMediaProvider.java | 399 | ||||
-rw-r--r-- | apex/framework/java/android/provider/CloudMediaProviderContract.java | 469 | ||||
-rw-r--r-- | apex/framework/java/android/provider/CmpApiVerifier.java | 190 |
4 files changed, 1087 insertions, 9 deletions
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt index 4248c91f7..0290f7c16 100644 --- a/apex/framework/api/current.txt +++ b/apex/framework/api/current.txt @@ -16,6 +16,12 @@ package android.provider { method @NonNull public android.database.Cursor onQueryAlbums(@NonNull android.os.Bundle); method @NonNull public abstract android.database.Cursor onQueryDeletedMedia(@NonNull android.os.Bundle); method @NonNull public abstract android.database.Cursor onQueryMedia(@NonNull android.os.Bundle); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.database.Cursor onQueryMediaCategories(@Nullable String, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.database.Cursor onQueryMediaInMediaSet(@NonNull String, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.database.Cursor onQueryMediaSets(@NonNull String, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.database.Cursor onQuerySearchSuggestions(@NonNull String, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.database.Cursor onSearchMedia(@NonNull String, @Nullable String, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal); + method @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") @NonNull public android.database.Cursor onSearchMedia(@NonNull String, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal); method @NonNull public final android.os.ParcelFileDescriptor openFile(@NonNull android.net.Uri, @NonNull String) throws java.io.FileNotFoundException; method @NonNull public final android.os.ParcelFileDescriptor openFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException; method @NonNull public final android.content.res.AssetFileDescriptor openTypedAssetFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.Bundle) throws java.io.FileNotFoundException; @@ -59,10 +65,18 @@ package android.provider { field public static final String EXTRA_PAGE_SIZE = "android.provider.extra.PAGE_SIZE"; field public static final String EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN"; field public static final String EXTRA_PREVIEW_THUMBNAIL = "android.provider.extra.PREVIEW_THUMBNAIL"; + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final String EXTRA_SORT_ORDER = "android.provider.extra.SORT_ORDER"; field public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED"; field public static final String EXTRA_SYNC_GENERATION = "android.provider.extra.SYNC_GENERATION"; field public static final String MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION = "com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"; + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS = 1; // 0x1 field public static final String PROVIDER_INTERFACE = "android.content.action.CLOUD_MEDIA_PROVIDER"; + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int SEARCH_SUGGESTION_ALBUM = 4; // 0x4 + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int SEARCH_SUGGESTION_DATE = 3; // 0x3 + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int SEARCH_SUGGESTION_FACE = 1; // 0x1 + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int SEARCH_SUGGESTION_LOCATION = 2; // 0x2 + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int SEARCH_SUGGESTION_TEXT = 0; // 0x0 + field @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final int SORT_ORDER_DESC_DATE_TAKEN = 1; // 0x1 } public static final class CloudMediaProviderContract.AlbumColumns { @@ -73,6 +87,16 @@ package android.provider { field public static final String MEDIA_COVER_ID = "album_media_cover_id"; } + @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"; + field public static final String MEDIA_CATEGORY_TYPE = "media_category_type"; + field public static final String MEDIA_COVER_ID1 = "media_cover_id1"; + field public static final String MEDIA_COVER_ID2 = "media_cover_id2"; + field public static final String MEDIA_COVER_ID3 = "media_cover_id3"; + field public static final String MEDIA_COVER_ID4 = "media_cover_id4"; + } + public static final class CloudMediaProviderContract.MediaCollectionInfo { field public static final String ACCOUNT_CONFIGURATION_INTENT = "account_configuration_intent"; field public static final String ACCOUNT_NAME = "account_name"; @@ -99,6 +123,20 @@ package android.provider { field public static final String WIDTH = "width"; } + @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final class CloudMediaProviderContract.MediaSetColumns { + field public static final String DISPLAY_NAME = "display_name"; + field public static final String ID = "id"; + field public static final String MEDIA_COUNT = "media_count"; + field public static final String MEDIA_COVER_ID = "media_cover_id"; + } + + @FlaggedApi("com.android.providers.media.flags.cloud_media_provider_search") public static final class CloudMediaProviderContract.SearchSuggestionColumns { + field public static final String DISPLAY_TEXT = "display_text"; + field public static final String MEDIA_COVER_ID = "media_cover_id"; + field public static final String MEDIA_SET_ID = "media_set_id"; + field public static final String TYPE = "type"; + } + public final class MediaStore { ctor public MediaStore(); method public static boolean canManageMedia(@NonNull android.content.Context); diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java index 7875fdf3b..932421c9a 100644 --- a/apex/framework/java/android/provider/CloudMediaProvider.java +++ b/apex/framework/java/android/provider/CloudMediaProvider.java @@ -25,16 +25,27 @@ import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB 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; +import static android.provider.CloudMediaProviderContract.KEY_MEDIA_CATEGORY_ID; +import static android.provider.CloudMediaProviderContract.KEY_MEDIA_SET_ID; +import static android.provider.CloudMediaProviderContract.KEY_PARENT_CATEGORY_ID; +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_MEDIA_COLLECTION_INFO; import static android.provider.CloudMediaProviderContract.URI_PATH_ALBUM; import static android.provider.CloudMediaProviderContract.URI_PATH_DELETED_MEDIA; import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA; +import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY; import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO; +import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_SET; +import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET; +import static android.provider.CloudMediaProviderContract.URI_PATH_SEARCH_MEDIA; +import static android.provider.CloudMediaProviderContract.URI_PATH_SEARCH_SUGGESTION; import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER; import android.annotation.DurationMillisLong; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -62,6 +73,8 @@ import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; +import com.android.providers.media.flags.Flags; + import java.io.FileNotFoundException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -118,6 +131,11 @@ public abstract class CloudMediaProvider extends ContentProvider { private static final int MATCH_ALBUMS = 3; private static final int MATCH_MEDIA_COLLECTION_INFO = 4; private static final int MATCH_SURFACE_CONTROLLER = 5; + private static final int MATCH_MEDIA_CATEGORIES = 6; + private static final int MATCH_MEDIA_SETS = 7; + private static final int MATCH_SEARCH_SUGGESTION = 8; + private static final int MATCH_SEARCH = 9; + private static final int MATCH_MEDIAS_IN_MEDIA_SET = 10; private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true; private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false; @@ -145,6 +163,11 @@ public abstract class CloudMediaProvider extends ContentProvider { mMatcher.addURI(authority, URI_PATH_ALBUM, MATCH_ALBUMS); mMatcher.addURI(authority, URI_PATH_MEDIA_COLLECTION_INFO, MATCH_MEDIA_COLLECTION_INFO); mMatcher.addURI(authority, URI_PATH_SURFACE_CONTROLLER, MATCH_SURFACE_CONTROLLER); + mMatcher.addURI(authority, URI_PATH_MEDIA_CATEGORY, MATCH_MEDIA_CATEGORIES); + mMatcher.addURI(authority, URI_PATH_MEDIA_SET, MATCH_MEDIA_SETS); + mMatcher.addURI(authority, URI_PATH_SEARCH_SUGGESTION, MATCH_SEARCH_SUGGESTION); + mMatcher.addURI(authority, URI_PATH_SEARCH_MEDIA, MATCH_SEARCH); + mMatcher.addURI(authority, URI_PATH_MEDIA_IN_MEDIA_SET, MATCH_MEDIAS_IN_MEDIA_SET); } /** @@ -275,6 +298,306 @@ public abstract class CloudMediaProvider extends ContentProvider { } /** + * Queries for the available MediaCategories under the given {@code parentCategoryId}, + * filtered by {@code extras}. The columns of MediaCategories are + * in the class {@link CloudMediaProviderContract.MediaCategoryColumns}. + * + * When {@code parentCategoryId} is null, this returns the root categories. + * <p> + * The order in which media categories are sorted in the cursor + * will be retained when displaying results to the user. + * <p> + * The cloud media provider must set the + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor + * by using {@link Cursor#setExtras}. Not setting this is an error and invalidates the + * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. + * <p> + * {@code extras} may contain some key-value pairs which should be used to filter the results. + * If the provider handled any filters in {@code extras}, it must add the key to + * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using + * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is + * without the extra being used. + * Note: Currently this function does not pass any params in {@code extras}. + * + * @param parentCategoryId the ID of the parent category to filter media categories. + * @param extras containing keys to filter media categories. + * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. + * @return cursor with {@link CloudMediaProviderContract.MediaCategoryColumns} columns + */ + // TODO(b/358309179): Add a description of timely response from the provider and a threshold + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + @NonNull + public Cursor onQueryMediaCategories(@Nullable String parentCategoryId, + @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { + throw new UnsupportedOperationException("queryMediaCategories not supported"); + } + + /** + * Queries for the available MediaSets under a given {@code mediaCategoryId}, + * filtered by {@code extras}. The columns of MediaSet are in the class + * {@link CloudMediaProviderContract.MediaSetColumns}. + * + * This returns MediaSets directly inside the given MediaCategoryId. + * If the passed mediaCategoryId has some more nested mediaCategories, the mediaSets inside + * the nested mediaCategories must not be returned in this response. + * + * <p> + * The order in which media sets are sorted in the cursor + * will be retained when displaying results to the user. + * <p> + * The cloud media provider must set the + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor + * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the + * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. + * <p> + * + * {@code extras} may contain some key-value pairs which should be used to prepare the results. + * If the provider handled any filters in {@code extras}, it must add the key to + * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using + * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is + * without the extra being used. + * + * <p> + * If the cloud media provider supports pagination, they can set + * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, + * as part of the returned cursor by using {@link Cursor#setExtras}. + * + * If a token is set, the OS will pass it as a key-value pair in {@code extras} + * when querying for query media sets for subsequent pages. + * + * The provider can keep returning pagination tokens in the returned cursor + * by using {@link Cursor#setExtras} until the last page at which point it should not + * set a token in the returned cursor. + * + * @param mediaCategoryId the ID of the media category to filter media sets. + * @param extras containing keys to filter media sets: + * <ul> + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} + * </ul> + * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. + * @return cursor representing {@link CloudMediaProviderContract.MediaSetColumns} columns + */ + // TODO(b/358309179): Add a description of timely response from the provider and a threshold + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + @NonNull + public Cursor onQueryMediaSets(@NonNull String mediaCategoryId, + @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { + throw new UnsupportedOperationException("queryMediaSets not supported"); + } + + /** + * Queries for the available SearchSuggestions based on a {@code prefixText}, + * filtered by {@code extras}. The columns of SearchSuggestions are in the class + * {@link CloudMediaProviderContract.SearchSuggestionColumns} + * + * If the user has not started typing, this is considered as zero state suggestion. + * In this case {@code prefixText} will be empty string. + * + * <p> + * The order in which suggestions are sorted in the cursor + * will be retained when displaying results to the user. + * <p> + * The cloud media provider must set the + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor + * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the + * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. + * <p> + * {@code extras} may contain some key-value pairs which should be used to prepare + * the results. + * If the provider handled any params in {@code extras}, it must add the key to + * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using + * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is + * without the extra being used. + * Note: Currently this function does not pass any key-value params in {@code extras}. + * + * @param prefixText the prefix text to filter search suggestions. + * @param extras containing keys to filter search suggestions. + * <ul> + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} + * </ul> + * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. + * @return cursor representing search suggestions containing all + * {@see CloudMediaProviderContract.SearchSuggestionColumns} columns + */ + // TODO(b/358309179): Add a description of timely response from the provider and a threshold + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + @NonNull + public Cursor onQuerySearchSuggestions(@NonNull String prefixText, + @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { + throw new UnsupportedOperationException("querySearchSuggestions not supported"); + } + + /** + * Queries for the available media items under a given {@code mediaSetId}, + * filtered by {@code extras}. The columns of Media are in the class + * {@link CloudMediaProviderContract.MediaColumns}. {@code mediaSetId} is the ID given + * as part of {@link CloudMediaProviderContract.MediaSetColumns#ID} + * + * <p> + * The order in which media items are sorted in the cursor + * will be retained when displaying results to the user. + * <p> + * The cloud media provider must set the + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned + * {@link Cursor} by using {@link Cursor#setExtras}. + * Not setting this is an error and invalidates the + * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. + * + * <p> + * {@code extras} may contain some key-value pairs which should be used to prepare the results. + * If the provider handled any filters in {@code extras}, it must add the key to + * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using + * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is + * without the extra being used. + * + * <p> + * If the cloud media provider supports pagination, they can set + * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, + * as part of the returned cursor by using {@link Cursor#setExtras}. + * + * If a token is set, the OS will pass it as a key-value pair in {@code extras} + * when querying for media for subsequent pages. + * + * The provider can keep returning pagination tokens in the returned cursor + * by using {@link Cursor#setExtras} until the last page at which point it should not + * set a token in the returned cursor. + * + * @param mediaSetId the ID of the media set to filter media items. + * @param extras containing keys to filter media items: + * <ul> + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} + * <li> {@link CloudMediaProviderContract#EXTRA_SORT_ORDER} + * </ul> + * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. + * @return cursor representing {@link CloudMediaProviderContract.MediaColumns} columns + */ + // TODO(b/358309179): Add a description of timely response from the provider and a threshold + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + @NonNull + public Cursor onQueryMediaInMediaSet(@NonNull String mediaSetId, + @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { + throw new UnsupportedOperationException("queryMedia in MediaSet not supported"); + } + + /** + * Searches media items based on a selected suggestion, managed by {@code extras} and + * returns a cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. + * + * <p> + * The cloud media provider must set the + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor + * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the + * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. + * <p> + * + * {@code extras} may contain some key-value pairs which should be used to prepare the results. + * If the provider handled any params in {@code extras}, it must add the key to + * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using + * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is + * without the extra being used. + * + * <p> + * An example user journey: + * 1. User enters the search prompt. + * 2. Using {@link #onQuerySearchSuggestions}, + * photo picker display suggestions as the user keeps typing. + * 3. User selects a suggestion. + * Photo picker calls: {@code onSearchMedia(suggestedMediaSetId, fallbackSearchText, extras)} + * with the {@code suggestedMediaSetId} corresponding to the user chosen suggestion. + * {@link CloudMediaProviderContract.SearchSuggestionColumns#MEDIA_SET_ID} + * + * <p> + * If the cloud media provider supports pagination, they can set + * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, + * as part of the returned cursor by using {@link Cursor#setExtras}. + * + * If a token is set, the OS will pass it as a key value pair in {@code extras} + * when querying for search media for subsequent pages. + * + * The provider can keep returning pagination tokens in the returned cursor + * by using {@link Cursor#setExtras} until the last page at which point it should not + * set a token in the returned cursor + * + * @param suggestedMediaSetId the media set ID of the suggestion that the user wants to search. + * @param fallbackSearchText optional search text to be used when {@code suggestedMediaSetId} + * is not useful. + * @param extras containing keys to manage the search results: + * <ul> + * <li>{@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} + * <li>{@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} + * <li>{@link CloudMediaProviderContract#EXTRA_SORT_ORDER} + * </ul> + * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. + * @return cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. + * @see CloudMediaProviderContract.SearchSuggestionColumns#MEDIA_SET_ID + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + @NonNull + public Cursor onSearchMedia(@NonNull String suggestedMediaSetId, + @Nullable String fallbackSearchText, + @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { + throw new UnsupportedOperationException("searchMedia with" + + " suggestedMediaSetId not supported"); + } + + /** + * Searches media items based on entered search text, managed by {@code extras} and + * returns a cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. + * + * <p> + * The cloud media provider must set the + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor + * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the + * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. + * <p> + * + * {@code extras} may contain some key-value pairs which should be used to prepare the results. + * If the provider handled any params in {@code extras}, it must add the key to + * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using + * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is + * without the extra being used. + * + * <p> + * An example user journey: + * 1. User enters the search prompt. + * 2. Using {@link #onQuerySearchSuggestions}, + * photo picker display suggestions as the user keeps typing. + * 3. User types completely and then enters search, + * Photo picker calls: {@code onSearchMedia(searchText, extras)} + * + * <p> + * If the cloud media provider supports pagination, they can set + * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, + * as part of the returned cursor by using {@link Cursor#setExtras}. + * + * If a token is set, the OS will pass it as a key value pair in {@code extras} + * when querying for search media for subsequent pages. + * + * The provider can keep returning pagination tokens in the returned cursor + * by using {@link Cursor#setExtras} until the last page at which point it should not + * set a token in the returned cursor. + * + * @param searchText search text to be used. + * @param extras containing keys to manage the search results: + * <ul> + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} + * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} + * <li> {@link CloudMediaProviderContract#EXTRA_SORT_ORDER} + * </ul> + * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. + * @return cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. + */ + // TODO(b/358309179): Add a description of timely response from the provider and a threshold + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + @NonNull + public Cursor onSearchMedia(@NonNull String searchText, + @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { + throw new UnsupportedOperationException("searchMediaFromText not supported"); + } + + /** * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId} * <p>The cloud media provider should strictly return thumbnail in the original * {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item. @@ -538,6 +861,82 @@ public abstract class CloudMediaProvider extends ContentProvider { CmpApiVerifier.CloudMediaProviderApis.OnQueryAlbums, result), System.currentTimeMillis() - startTime, mAuthority); break; + case MATCH_MEDIA_CATEGORIES: + if (Flags.cloudMediaProviderSearch()) { + final String parentCategoryId = queryArgs.getString(KEY_PARENT_CATEGORY_ID); + queryArgs.remove(KEY_PARENT_CATEGORY_ID); + result = onQueryMediaCategories(parentCategoryId, queryArgs, cancellationSignal + ); + CmpApiVerifier.verifyApiResult(new CmpApiResult( + CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaCategories, + result), + System.currentTimeMillis() - startTime, mAuthority); + } else { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + break; + case MATCH_MEDIA_SETS: + if (Flags.cloudMediaProviderSearch()) { + final String mediaCategoryId = queryArgs.getString(KEY_MEDIA_CATEGORY_ID); + queryArgs.remove(KEY_MEDIA_CATEGORY_ID); + result = onQueryMediaSets(mediaCategoryId, queryArgs, cancellationSignal); + CmpApiVerifier.verifyApiResult(new CmpApiResult( + CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaSets, + result), + System.currentTimeMillis() - startTime, mAuthority); + } else { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + break; + case MATCH_SEARCH_SUGGESTION: + if (Flags.cloudMediaProviderSearch()) { + final String prefixText = queryArgs.getString(KEY_PREFIX_TEXT); + queryArgs.remove(KEY_PREFIX_TEXT); + result = onQuerySearchSuggestions(prefixText, queryArgs, cancellationSignal); + CmpApiVerifier.verifyApiResult(new CmpApiResult( + CmpApiVerifier.CloudMediaProviderApis.OnQuerySearchSuggestions, + result), + System.currentTimeMillis() - startTime, mAuthority); + } else { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + break; + case MATCH_MEDIAS_IN_MEDIA_SET: + if (Flags.cloudMediaProviderSearch()) { + final String mediaSetId = queryArgs.getString(KEY_MEDIA_SET_ID); + queryArgs.remove(KEY_MEDIA_SET_ID); + result = onQueryMediaInMediaSet(mediaSetId, queryArgs, cancellationSignal); + CmpApiVerifier.verifyApiResult(new CmpApiResult( + CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaInMediaSet, + result), + System.currentTimeMillis() - startTime, mAuthority); + } else { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + break; + case MATCH_SEARCH: + if (Flags.cloudMediaProviderSearch()) { + final String searchText = queryArgs.getString(KEY_SEARCH_TEXT); + queryArgs.remove(KEY_SEARCH_TEXT); + final String mediaSetId = queryArgs.getString(KEY_MEDIA_SET_ID); + queryArgs.remove(KEY_MEDIA_SET_ID); + if (mediaSetId != null) { + result = onSearchMedia(mediaSetId, searchText, queryArgs, cancellationSignal + ); + } else if (searchText != null) { + result = onSearchMedia(searchText, queryArgs, cancellationSignal); + } else { + throw new IllegalArgumentException("both suggested media set id " + + "and search text can not be null together"); + } + CmpApiVerifier.verifyApiResult(new CmpApiResult( + CmpApiVerifier.CloudMediaProviderApis.OnSearchMedia, + result), + System.currentTimeMillis() - startTime, mAuthority); + } else { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + break; default: throw new UnsupportedOperationException("Unsupported Uri " + uri); } diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java index 5e610a86c..4625503ca 100644 --- a/apex/framework/java/android/provider/CloudMediaProviderContract.java +++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java @@ -16,12 +16,19 @@ package android.provider; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; +import com.android.providers.media.flags.Flags; + +import java.lang.annotation.Retention; import java.util.UUID; /** @@ -593,6 +600,37 @@ public final class CloudMediaProviderContract { "android.provider.extra.PREVIEW_THUMBNAIL"; /** + * Extra used to specify the sorting behavior when querying from {@link CloudMediaProvider}. + * The value associated with this extra should be one of the integer constants + * defined in the {@link SortOrders}. + * <p> + * Type: INTEGER + * + * @see CloudMediaProvider#onSearchMedia + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final String EXTRA_SORT_ORDER = "android.provider.extra.SORT_ORDER"; + + /** + * Sort items in descending order by the {@code DATE_TAKEN_MILLIS}. + * <p> + * This means the most recently taken photos or videos will appear first. + * <p> + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int SORT_ORDER_DESC_DATE_TAKEN = 1; + + /** + * Defines integer constants to be used with the {@link #EXTRA_SORT_ORDER} extra + * for specifying the sorting order of media items. + * @hide + */ + @IntDef(value = {SORT_ORDER_DESC_DATE_TAKEN}) + @Retention(SOURCE) + public @interface SortOrder {} + + /** * A boolean to indicate {@link com.android.providers.media.photopicker.PhotoPickerProvider} * this request is requesting a cached thumbnail file from MediaStore. * @@ -734,4 +772,435 @@ public final class CloudMediaProviderContract { * {@hide} */ public static final String URI_PATH_SURFACE_CONTROLLER = "surface_controller"; + + /** + * URI path for {@link CloudMediaProvider#onQueryMediaCategories} + * + * @hide + */ + public static final String URI_PATH_MEDIA_CATEGORY = "media_category"; + + /** + * URI path for {@link CloudMediaProvider#onQueryMediaSets} + * + * @hide + */ + public static final String URI_PATH_MEDIA_SET = "media_set"; + + /** + * URI path for {@link CloudMediaProvider#onQuerySearchSuggestions} + * + * @hide + */ + public static final String URI_PATH_SEARCH_SUGGESTION = "search_suggestion"; + + /** + * URI path for {@link CloudMediaProvider#onSearchMedia} + * + * @hide + */ + public static final String URI_PATH_SEARCH_MEDIA = "search_media"; + + /** + * URI path for {@link CloudMediaProvider#onQueryMediaInMediaSet} + * + * @hide + */ + public static final String URI_PATH_MEDIA_IN_MEDIA_SET = + "query_media_in_media_set"; + + /** + * Key for passing parent category Id as a parameter in the bundle + * + * @hide + */ + public static final String KEY_PARENT_CATEGORY_ID = "parent_category_id"; + + /** + * Key for passing media category Id as a parameter in the bundle + * + * @hide + */ + public static final String KEY_MEDIA_CATEGORY_ID = "media_category_id"; + + /** + * Key for passing media set Id as a parameter in the bundle + * + * @hide + */ + public static final String KEY_MEDIA_SET_ID = "media_set_id"; + + /** + * Key for passing prefix text as a parameter in the bundle + * + * @hide + */ + public static final String KEY_PREFIX_TEXT = "prefix_text"; + + /** + * Key for passing search query as a parameter in the bundle + * + * @hide + */ + public static final String KEY_SEARCH_TEXT = "search_text"; + + /** + * MediaSet represents a cohesive collection of related unique media items, + * sharing a common meaningful context or theme. + * This is the basic and fundamental unit for organizing related media items. + * + * MediaSet in this context is represented + * by a set of columns present in {@link MediaSetColumns} + * + * Examples of media sets include: + * <ul> + * <li>Faces of the same person</li> + * <li>Photos of a specific location</li> + * <li>All media as a search result to mountains</li> + * </ul> + * + * Note: {@link AlbumColumns} which denotes an album can also be represented + * using {@link MediaSetColumns}. But, it is recommended to keep using {@link AlbumColumns} + * for existing user albums and use MediaSet only for supported MediaCategories . + * + * The currently supported MediaCategory in photo picker are + * {@link #MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS}. + * + * These are the fields of a MediaSet. + * + * @see MediaCategoryColumns + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final class MediaSetColumns { + + private MediaSetColumns() {} + + /** + * Unique ID of the media set. This ID is both provided by and interpreted + * by the {@link CloudMediaProvider}. + * + * Each media set must have a unique ID. + * + * A provider should return IDs which are stable, + * meaning it remains the same if nothing inside it changes, + * since they will be used to cache media set information in the OS. + * + * Type: STRING + */ + public static final String ID = "id"; + + /** + * Display name of the media set. + * This display name provided should match the current devices locale settings. + * If there is no display name, pass {@code null} in this column. + * + * Type: STRING + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * Total count of all media within the media set, including photos and videos. + * + * If this field is not provided, + * media sets will be shown without a count in the Photo Picker. + * + * Type: LONG + */ + public static final String MEDIA_COUNT = "media_count"; + + /** + * Media ID to use as the media set cover photo. + * + * If this field is not provided, + * media sets will be shown in the Photo Picker with a default icon. + * + * Type: STRING + * + * @see CloudMediaProviderContract.MediaColumns#ID + */ + public static final String MEDIA_COVER_ID = "media_cover_id"; + + /** + * Contains all column names for {@link MediaSetColumns} as an array. + * @hide + */ + public static final String[] ALL_PROJECTION = new String[] { + MediaSetColumns.ID, + MediaSetColumns.DISPLAY_NAME, + MediaSetColumns.MEDIA_COUNT, + MediaSetColumns.MEDIA_COVER_ID + }; + } + + /** + * MediaCategory represents a broader structure + * that a {@link MediaSetColumns} or another {@link MediaCategoryColumns} belongs to. + * + * A MediaCategory in this context is represented by a set of columns present in + * {@link MediaCategoryColumns} + * + * A MediaCategory can have instances of other MediaCategories + * to support a multilevel hierarchy. + * Examples of MediaCategory: + * <ul> + * <li>A MediaCategory of people and pet faces which contains instances of MediaSets + * for different faces</li> + * <li>A MediaCategory of locations which contains instances of MediaSets for + * different locations</li> + * </ul> + * + * The currently supported MediaCategory in photo picker are + * {@link #MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS}. + * + * These are the fields of MediaCategory. + * @see CloudMediaProvider#onQueryMediaCategories + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final class MediaCategoryColumns { + + private MediaCategoryColumns() {} + + /** + * The unique identifier of the media category. + * This ID is both provided by and interpreted by the {@link CloudMediaProvider}. + * + * A provider should return IDs which are stable, + * meaning it remains the same if nothing inside it changes, + * since they will be used to cache information in the OS. + * + * Type: STRING + */ + public static final String ID = "id"; + + /** + * The display name of the media category. + * This display name provided should match the current devices locale settings. + * + * If there is no display name, pass {@code null} in this column. + * + * Type: STRING + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * 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} + * + * Type: INTEGER + */ + public static final String MEDIA_CATEGORY_TYPE = "media_category_type"; + + /** + * The first cover media ID for displaying. + * <p> + * If none of the MEDIA_COVER_ID is provided, + * media category will be shown in the Photo Picker with a default icon. + * Otherwise, Photo Picker will show as many MEDIA_COVER_IDs as provided. + * <p> + * Type: STRING + */ + public static final String MEDIA_COVER_ID1 = "media_cover_id1"; + + /** + * The second cover media ID for displaying. + * <p> + * If none of the MEDIA_COVER_ID is provided, + * media category will be shown in the Photo Picker with a default icon. + * Otherwise, Photo Picker will show as many MEDIA_COVER_IDs as provided. + * <p> + * Type: STRING + */ + public static final String MEDIA_COVER_ID2 = "media_cover_id2"; + + /** + * The third cover media ID for displaying. + * <p> + * If none of the MEDIA_COVER_ID is provided, + * media category will be shown in the Photo Picker with a default icon. + * Otherwise, Photo Picker will show as many MEDIA_COVER_IDs as provided. + * <p> + * Type: STRING + */ + public static final String MEDIA_COVER_ID3 = "media_cover_id3"; + + /** + * The fourth cover media ID for displaying. + * <p> + * If none of the MEDIA_COVER_ID is provided, + * media category will be shown in the Photo Picker with a default icon. + * Otherwise, Photo Picker will show as many MEDIA_COVER_IDs as provided. + * <p> + * Type: STRING + */ + public static final String MEDIA_COVER_ID4 = "media_cover_id4"; + + /** + * Contains all column names for {@link MediaCategoryColumns} as an array. + * + * @hide + */ + public static final String[] ALL_PROJECTION = new String[] { + MediaCategoryColumns.ID, + MediaCategoryColumns.DISPLAY_NAME, + MediaCategoryColumns.MEDIA_CATEGORY_TYPE, + MediaCategoryColumns.MEDIA_COVER_ID1, + MediaCategoryColumns.MEDIA_COVER_ID2, + MediaCategoryColumns.MEDIA_COVER_ID3, + MediaCategoryColumns.MEDIA_COVER_ID4 + }; + + } + + /** + * Represents media category related to faces of people and pets. + * @see MediaCategoryColumns#MEDIA_CATEGORY_TYPE + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS = 1; + + /** + * 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}) + @Retention(SOURCE) + public @interface MediaCategoryTypes {} + + /** + * Represents a search suggestion provided by the {@link CloudMediaProvider}. + * This is based on the user entered query. + * When the input query is empty (zero state), the provider can still return suggestions. + * Photo picker will show these zero state suggestions to the user, + * when nothing has been typed for search. + * + * This class contains the fields of SearchSuggestion. + * + * @see CloudMediaProvider#onQuerySearchSuggestions + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final class SearchSuggestionColumns { + + private SearchSuggestionColumns() {} + + /** + * The unique identifier of the media set associated with the search suggestion. + * This will be used to query media items if user clicked on this suggestion. + * + * <p> + * Type: STRING + * + * @see MediaSetColumns#ID + */ + public static final String MEDIA_SET_ID = "media_set_id"; + /** + * The display text for the search suggestion. + * <p> + * This is the text shown to the user as a suggestion. + * Display text provided should match the current devices locale settings. + * + * If no display text, pass {@code null} in this column. + * + * <p> + * Type: STRING + */ + public static final String DISPLAY_TEXT = "display_text"; + /** + * The type of the search suggestion. + * <p> + * This must contain one of the values from various supported search suggestion types. + * These are: {@link #SEARCH_SUGGESTION_TEXT}, {@link #SEARCH_SUGGESTION_FACE}, + * {@link #SEARCH_SUGGESTION_DATE}, {@link #SEARCH_SUGGESTION_LOCATION}, + * {@link #SEARCH_SUGGESTION_ALBUM} + * <p> + * This will be used to display to user different suggestions in different way. + * As examples: for Location type, a thumbnail of location will be used. + * For faces, face cover id (if provided) will be used. + * Type: INTEGER + */ + public static final String TYPE = "type"; + + /** + * Media ID to use as the cover image for the search suggestion. + * <p> + * If this field is not provided, + * the search suggestion will be shown with a default cover. + * <p> + * Type: LONG + */ + public static final String MEDIA_COVER_ID = "media_cover_id"; + + /** + * Contains all column names for {@link SearchSuggestionColumns} as an array. + * + * @hide + */ + public static final String[] ALL_PROJECTION = new String[] { + SearchSuggestionColumns.MEDIA_SET_ID, + SearchSuggestionColumns.DISPLAY_TEXT, + SearchSuggestionColumns.TYPE, + SearchSuggestionColumns.MEDIA_COVER_ID + }; + } + + /** + * Represents a generic text search suggestion. + * @see SearchSuggestionColumns#TYPE + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int SEARCH_SUGGESTION_TEXT = 0; + + /** + * Suggestion based on faces detected in photos. + * @see SearchSuggestionColumns#TYPE + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int SEARCH_SUGGESTION_FACE = 1; + + /** + * Suggestion based on location data associated with photos. + * @see SearchSuggestionColumns#TYPE + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int SEARCH_SUGGESTION_LOCATION = 2; + + /** + * Suggestion based on the date photos were taken. + * @see SearchSuggestionColumns#TYPE + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int SEARCH_SUGGESTION_DATE = 3; + + + /** + * Suggestion based on user albums. + * @see SearchSuggestionColumns#TYPE + * Type: INTEGER + */ + @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) + public static final int SEARCH_SUGGESTION_ALBUM = 4; + + /** + * Defines the different types of search suggestions available and supported in photo picker. + * + * @see SearchSuggestionColumns#TYPE + * @hide + */ + @IntDef(value = { + SEARCH_SUGGESTION_TEXT, + SEARCH_SUGGESTION_FACE, + SEARCH_SUGGESTION_LOCATION, + SEARCH_SUGGESTION_DATE, + SEARCH_SUGGESTION_ALBUM + }) + @Retention(SOURCE) + public @interface SEARCH_SUGGESTION_ALBUM {} } diff --git a/apex/framework/java/android/provider/CmpApiVerifier.java b/apex/framework/java/android/provider/CmpApiVerifier.java index dc948d4f1..4977d45be 100644 --- a/apex/framework/java/android/provider/CmpApiVerifier.java +++ b/apex/framework/java/android/provider/CmpApiVerifier.java @@ -36,10 +36,13 @@ import android.os.ParcelFileDescriptor; import android.os.SystemProperties; import android.util.Log; +import com.android.providers.media.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -104,6 +107,30 @@ final class CmpApiVerifier { errors); break; } + case CloudMediaProviderApis.OnQueryMediaCategories: { + verifyOnQueryMediaCategories(result.getCursor(), + verificationResult, errors); + break; + } + case CloudMediaProviderApis.OnQueryMediaSets: { + verifyOnQueryMediaSets(result.getCursor(), + verificationResult, errors); + break; + } + case CloudMediaProviderApis.OnQuerySearchSuggestions: { + verifyOnQuerySearchSuggestions(result.getCursor(), + verificationResult, errors); + break; + } + case CloudMediaProviderApis.OnSearchMedia: { + verifyOnSearchMedia(result.getCursor(), + verificationResult, errors); + break; + } + case CloudMediaProviderApis.OnQueryMediaInMediaSet: { + verifyOnQueryMediaInMediaSet(result.getCursor(), + verificationResult, errors); + } default: throw new UnsupportedOperationException( "The verification for requested API is not supported."); @@ -314,6 +341,135 @@ final class CmpApiVerifier { } } + /** + * Verifies OnQueryMediaCategories API by performing and logging the following checks: + * + * <ul> + * <li>Received Cursor is not null.</li> + * <li>Cursor contains non empty media collection ID: + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> + * <li>Projection for cursor is as expected: + * {@link CloudMediaProviderContract.MediaCategoryColumns#ALL_PROJECTION}</li> + * </ul> + */ + static void verifyOnQueryMediaCategories( + Cursor cursor, List<String> verificationResult, List<String> errors + ) { + verifyCursorNotNullAndMediaCollectionIdPresent(cursor, verificationResult, errors); + if (cursor != null && Flags.cloudMediaProviderSearch()) { + + verifyProjectionForCursor( + cursor, + Arrays.asList(CloudMediaProviderContract.MediaCategoryColumns.ALL_PROJECTION), + errors + ); + } + } + + /** + * Verifies OnQueryMediaSets API by performing and logging the following checks: + * + * <ul> + * <li>Received Cursor is not null.</li> + * <li>Cursor contains non empty media collection ID: + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> + * <li>Projection for cursor is as expected: + * {@link CloudMediaProviderContract.MediaSetColumns#ALL_PROJECTION}</li> + * </ul> + */ + static void verifyOnQueryMediaSets( + Cursor cursor, List<String> verificationResult, List<String> errors + ) { + verifyCursorNotNullAndMediaCollectionIdPresent(cursor, verificationResult, errors); + if (cursor != null && Flags.cloudMediaProviderSearch()) { + + verifyProjectionForCursor( + cursor, + Arrays.asList(CloudMediaProviderContract.MediaSetColumns.ALL_PROJECTION), + errors + ); + } + } + + /** + * Verifies OnQuerySearchSuggestions API by performing and logging the following checks: + * + * <ul> + * <li>Received Cursor is not null.</li> + * <li>Cursor contains non empty media collection ID: + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> + * <li>Projection for cursor is as expected: + * {@link CloudMediaProviderContract.SearchSuggestionColumns#ALL_PROJECTION}</li> + * </ul> + */ + static void verifyOnQuerySearchSuggestions( + Cursor cursor, List<String> verificationResult, List<String> errors + ) { + verifyCursorNotNullAndMediaCollectionIdPresent(cursor, verificationResult, errors); + if (cursor != null && Flags.cloudMediaProviderSearch()) { + + verifyProjectionForCursor( + cursor, + Arrays.asList( + CloudMediaProviderContract.SearchSuggestionColumns.ALL_PROJECTION), + errors + ); + } + } + + /** + * Verifies OnSearchMedia API by performing and logging the following checks: + * + * <ul> + * <li>Received Cursor is not null.</li> + * <li>Cursor contains non empty media collection ID: + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> + * <li>Projection for cursor is as expected: + * {@link CloudMediaProviderContract.MediaColumns#ALL_PROJECTION}</li> + * </ul> + */ + static void verifyOnSearchMedia( + Cursor cursor, List<String> verificationResult, List<String> errors + ) { + verifyCursorNotNullAndMediaCollectionIdPresent(cursor, verificationResult, errors); + if (cursor != null && Flags.cloudMediaProviderSearch()) { + + verifyProjectionForCursor( + cursor, + Arrays.asList( + CloudMediaProviderContract.MediaColumns.ALL_PROJECTION), + errors + ); + } + } + + /** + * Verifies OnQueryMediaInMediaSet by performing and logging the following checks: + * + * <ul> + * <li>Received Cursor is not null.</li> + * <li>Cursor contains non empty media collection ID: + * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> + * <li>Projection for cursor is as expected: + * {@link CloudMediaProviderContract.MediaColumns#ALL_PROJECTION}</li> + * </ul> + */ + static void verifyOnQueryMediaInMediaSet( + Cursor cursor, List<String> verificationResult, List<String> errors + ) { + verifyCursorNotNullAndMediaCollectionIdPresent(cursor, verificationResult, errors); + if (cursor != null && Flags.cloudMediaProviderSearch()) { + + verifyProjectionForCursor( + cursor, + Arrays.asList( + CloudMediaProviderContract.MediaColumns.ALL_PROJECTION), + errors + ); + } + } + + /** * Verifies OnOpenPreview API by performing and logging the following checks: @@ -369,7 +525,12 @@ final class CmpApiVerifier { CloudMediaProviderApis.OnQueryDeletedMedia, CloudMediaProviderApis.OnQueryAlbums, CloudMediaProviderApis.OnOpenPreview, - CloudMediaProviderApis.OnOpenMedia + CloudMediaProviderApis.OnOpenMedia, + CloudMediaProviderApis.OnQueryMediaCategories, + CloudMediaProviderApis.OnQueryMediaSets, + CloudMediaProviderApis.OnQuerySearchSuggestions, + CloudMediaProviderApis.OnSearchMedia, + CloudMediaProviderApis.OnQueryMediaInMediaSet, }) @Retention(RetentionPolicy.SOURCE) @interface CloudMediaProviderApis { @@ -379,14 +540,25 @@ final class CmpApiVerifier { String OnQueryAlbums = "onQueryAlbums"; String OnOpenPreview = "onOpenPreview"; String OnOpenMedia = "onOpenMedia"; + String OnQueryMediaCategories = "onQueryMediaCategories"; + String OnQueryMediaSets = "onQueryMediaSets"; + String OnQuerySearchSuggestions = "onQuerySearchSuggestions"; + String OnSearchMedia = "onSearchMedia"; + String OnQueryMediaInMediaSet = "onQueryMediaInMediaSet"; } - private static final Map<String, Long> CMP_API_TO_THRESHOLD_MAP = Map.of( - CloudMediaProviderApis.OnGetMediaCollectionInfo, 200L, - CloudMediaProviderApis.OnQueryMedia, 500L, - CloudMediaProviderApis.OnQueryDeletedMedia, 500L, - CloudMediaProviderApis.OnQueryAlbums, 500L, - CloudMediaProviderApis.OnOpenPreview, 1000L, - CloudMediaProviderApis.OnOpenMedia, 1000L - ); + private static final Map<String, Long> CMP_API_TO_THRESHOLD_MAP = new HashMap<>(Map.ofEntries( + Map.entry(CloudMediaProviderApis.OnGetMediaCollectionInfo, 200L), + Map.entry(CloudMediaProviderApis.OnQueryMedia, 500L), + Map.entry(CloudMediaProviderApis.OnQueryDeletedMedia, 500L), + Map.entry(CloudMediaProviderApis.OnQueryAlbums, 500L), + Map.entry(CloudMediaProviderApis.OnOpenPreview, 1000L), + Map.entry(CloudMediaProviderApis.OnOpenMedia, 1000L), + Map.entry(CloudMediaProviderApis.OnQueryMediaCategories, 500L), + Map.entry(CloudMediaProviderApis.OnQueryMediaSets, 500L), + Map.entry(CloudMediaProviderApis.OnQuerySearchSuggestions, 500L), + Map.entry(CloudMediaProviderApis.OnSearchMedia, 1500L), + Map.entry(CloudMediaProviderApis.OnQueryMediaInMediaSet, 500L) + )); + } |