summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ashish Kumar <akgaurav@google.com> 2024-05-29 13:27:29 +0000
committer Ashish Kumar <akgaurav@google.com> 2024-10-04 03:51:23 +0000
commitabafc3b678b59bd89a531aa0c6a52342ae7989b0 (patch)
tree6b6a2a7654558e736123f17fbdf9bb5601eae556
parent77e89cb98b0da8909c2393d0fbf305b72ae48599 (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
-rw-r--r--apex/framework/api/current.txt38
-rw-r--r--apex/framework/java/android/provider/CloudMediaProvider.java399
-rw-r--r--apex/framework/java/android/provider/CloudMediaProviderContract.java469
-rw-r--r--apex/framework/java/android/provider/CmpApiVerifier.java190
-rw-r--r--mediaprovider_flags.aconfig9
-rw-r--r--src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java138
-rw-r--r--tests/AndroidManifest.xml6
-rw-r--r--tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java133
-rw-r--r--tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java137
9 files changed, 1510 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)
+ ));
+
}
diff --git a/mediaprovider_flags.aconfig b/mediaprovider_flags.aconfig
index d71fbb068..6bb51ed44 100644
--- a/mediaprovider_flags.aconfig
+++ b/mediaprovider_flags.aconfig
@@ -156,3 +156,12 @@ flag {
bug: "361026918"
is_fixed_read_only: true
}
+
+flag {
+ name: "cloud_media_provider_search"
+ is_exported: true
+ namespace: "mediaprovider"
+ is_fixed_read_only: true
+ description: "This flag will enable the apis for cloud media provider to donate search results"
+ bug: "316356081"
+}
diff --git a/src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java b/src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java
new file mode 100644
index 000000000..0857d2c9a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.v2;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.provider.CloudMediaProviderContract;
+import android.provider.CloudMediaProviderContract.SortOrder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A client class responsible for fetching search results from
+ * cloud media provider and local search provider.
+ */
+public class PickerSearchProviderClient {
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final String mCloudProviderAuthority;
+
+ private PickerSearchProviderClient(@NonNull Context context,
+ @NonNull String cloudProviderAuthority) {
+ mContext = requireNonNull(context);
+ mCloudProviderAuthority = requireNonNull(cloudProviderAuthority);
+ }
+
+ /**
+ * Create instance of a picker search client.
+ */
+ public static PickerSearchProviderClient create(@NonNull Context context,
+ @NonNull String cloudProviderAuthority) {
+ return new PickerSearchProviderClient(context, cloudProviderAuthority);
+ }
+
+ /**
+ * Method for querying CloudMediaProvider for media search result.
+ * Note: This functions does not expect pagination args.
+ */
+ @Nullable
+ public Cursor fetchSearchResultsFromCmp(@Nullable String suggestedMediaSetId,
+ @Nullable String searchText, @NonNull @SortOrder int sortOrder,
+ @Nullable CancellationSignal cancellationSignal) {
+ if (suggestedMediaSetId == null && searchText == null) {
+ throw new IllegalArgumentException(
+ "both suggestedMediaSet and searchText can not be null at once");
+ }
+ final Bundle queryArgs = new Bundle();
+ queryArgs.putString(CloudMediaProviderContract.KEY_SEARCH_TEXT, searchText);
+ queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_SET_ID, suggestedMediaSetId);
+ queryArgs.putInt(CloudMediaProviderContract.EXTRA_SORT_ORDER, sortOrder);
+
+ return mContext.getContentResolver().query(
+ getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_SEARCH_MEDIA),
+ null, queryArgs, cancellationSignal);
+ }
+
+ /**
+ * Method for querying CloudMediaProvider for search suggestions
+ */
+ @Nullable
+ public Cursor fetchSearchSuggestionsFromCmp(@NonNull String prefixText,
+ @Nullable CancellationSignal cancellationSignal) {
+ final Bundle queryArgs = new Bundle();
+ queryArgs.putString(CloudMediaProviderContract.KEY_PREFIX_TEXT, requireNonNull(prefixText));
+ return mContext.getContentResolver().query(
+ getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_SEARCH_SUGGESTION),
+ null, queryArgs, cancellationSignal);
+ }
+
+ /**
+ * Method for querying CloudMediaProvider for MediaCategories
+ */
+ @Nullable
+ public Cursor fetchMediaCategoriesFromCmp(@Nullable String parentCategoryId,
+ @Nullable CancellationSignal cancellationSignal) {
+ final Bundle queryArgs = new Bundle();
+ queryArgs.putString(CloudMediaProviderContract.KEY_PARENT_CATEGORY_ID, parentCategoryId);
+ return mContext.getContentResolver().query(
+ getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY),
+ null, queryArgs, cancellationSignal);
+ }
+
+ /**
+ * Method for querying CloudMediaProvider for MediaSets
+ */
+ @Nullable
+ public Cursor fetchMediaSetsFromCmp(@NonNull String mediaCategoryId,
+ @Nullable CancellationSignal cancellationSignal) {
+ final Bundle queryArgs = new Bundle();
+ queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_CATEGORY_ID,
+ requireNonNull(mediaCategoryId));
+ return mContext.getContentResolver().query(
+ getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_SET),
+ null, queryArgs, cancellationSignal);
+ }
+
+ /**
+ * Method for querying Medias inside a MediaSet
+ */
+ @Nullable
+ public Cursor fetchMediasInMediaSetFromCmp(@NonNull String mediaSetId,
+ @Nullable CancellationSignal cancellationSignal) {
+ final Bundle queryArgs = new Bundle();
+ queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_SET_ID,
+ requireNonNull(mediaSetId));
+ return mContext.getContentResolver().query(
+ getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET),
+ null, queryArgs, cancellationSignal);
+ }
+
+ private Uri getCloudUriFromPath(String uriPath) {
+ return Uri.parse("content://" + mCloudProviderAuthority + "/" + uriPath);
+ }
+
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index e785e929d..5c928b049 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -129,6 +129,12 @@
android:exported="true">
</provider>
+ <provider android:name="com.android.providers.media.photopickersearch.CloudMediaProviderSearch"
+ android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
+ android:authorities="com.android.providers.media.photopickersearch.tests.cloud_provider_search"
+ android:exported="true">
+ </provider>
+
<service
android:name=
"com.android.providers.media.stableuris.job.StableUriIdleMaintenanceService"
diff --git a/tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java b/tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java
new file mode 100644
index 000000000..11066118a
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopickersearch;
+
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.CloudMediaProvider;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileNotFoundException;
+
+public class CloudMediaProviderSearch extends CloudMediaProvider {
+
+ public static final String[] MEDIA_PROJECTIONS = new String[] {
+ CloudMediaProviderContract.MediaColumns.ID
+ };
+
+ public static final String TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH = "1";
+ public static final String TEST_MEDIA_ID_IN_MEDIA_SET = "2";
+ public static final String TEST_MEDIA_ID_FROM_TEXT_SEARCH = "3";
+
+ public static final String TEST_MEDIA_SET_ID = "11";
+ public static final String TEST_MEDIA_CATEGORY_ID = "111";
+ public static final String TEST_SEARCH_SUGGESTION_MEDIA_SET_ID = "2";
+
+ @Override
+ public Cursor onSearchMedia(String mediaSetId, String fallbackSearchText,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(MEDIA_PROJECTIONS);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onSearchMedia(String searchText,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(MEDIA_PROJECTIONS);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_ID_FROM_TEXT_SEARCH});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQueryMediaInMediaSet(String mediaSetId, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(MEDIA_PROJECTIONS);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_ID_IN_MEDIA_SET});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQueryMediaSets(String mediaCategoryId, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(
+ CloudMediaProviderContract.MediaSetColumns.ALL_PROJECTION);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_SET_ID, "Media Set 1", 1, 25});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQuerySearchSuggestions(String prefixText, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(
+ CloudMediaProviderContract.SearchSuggestionColumns.ALL_PROJECTION);
+ mockCursor.addRow(new Object[]{TEST_SEARCH_SUGGESTION_MEDIA_SET_ID,
+ 1, "song", "Suggestion 1 for " + prefixText});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQueryMediaCategories(String parentCategoryId,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor =
+ new MatrixCursor(CloudMediaProviderContract.MediaCategoryColumns.ALL_PROJECTION);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_CATEGORY_ID, 1, null, 1, 2, 3, 4});
+ return mockCursor;
+ }
+
+ @Override
+ public Bundle onGetMediaCollectionInfo(@NonNull Bundle extras) {
+ return new Bundle();
+ }
+
+ @Override
+ public Cursor onQueryMedia(@NonNull Bundle extras) {
+ return new MatrixCursor(new String[0]);
+ }
+
+ @Override
+ public Cursor onQueryDeletedMedia(@NonNull Bundle extras) {
+ return new MatrixCursor(new String[0]);
+ }
+
+ @Override
+ public AssetFileDescriptor onOpenPreview(@NonNull String mediaId, @NonNull Point size,
+ @Nullable Bundle extras, @Nullable CancellationSignal signal)
+ throws FileNotFoundException {
+ throw new UnsupportedOperationException("onOpenPreview not supported");
+ }
+
+ @Override
+ public ParcelFileDescriptor onOpenMedia(@NonNull String mediaId, @Nullable Bundle extras,
+ @Nullable CancellationSignal signal) throws FileNotFoundException {
+ throw new UnsupportedOperationException("onOpenMedia not supported");
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+}
diff --git a/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java b/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
new file mode 100644
index 000000000..4c90f54db
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopickersearch;
+
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.MEDIA_PROJECTIONS;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_CATEGORY_ID;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_ID_FROM_TEXT_SEARCH;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_ID_IN_MEDIA_SET;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_SET_ID;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_SEARCH_SUGGESTION_MEDIA_SET_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.photopicker.v2.PickerSearchProviderClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RequiresFlagsEnabled(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
+@RunWith(AndroidJUnit4.class)
+public class PickerSearchProviderClientTest {
+
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final String CLOUD_SEARCH_PROVIDER_AUTHORITY =
+ "com.android.providers.media.photopickersearch.tests.cloud_provider_search";
+
+ private PickerSearchProviderClient mPickerSearchProviderClient;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mPickerSearchProviderClient =
+ PickerSearchProviderClient.create(mContext, CLOUD_SEARCH_PROVIDER_AUTHORITY);
+ }
+
+ @Test
+ public void testFetchSearchSuggestionsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchSearchSuggestionsFromCmp("test",
+ null);
+ cursor.moveToFirst();
+ assertEquals(TEST_SEARCH_SUGGESTION_MEDIA_SET_ID, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.SearchSuggestionColumns.MEDIA_SET_ID)));
+ assertCursorColumns(cursor,
+ CloudMediaProviderContract.SearchSuggestionColumns.ALL_PROJECTION);
+ }
+
+ @Test
+ public void testFetchSuggestedSearchResultsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchSearchResultsFromCmp(
+ TEST_SEARCH_SUGGESTION_MEDIA_SET_ID, null, 1, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID)));
+ assertCursorColumns(cursor, MEDIA_PROJECTIONS);
+ }
+
+ @Test
+ public void testFetchTextSearchResultsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchSearchResultsFromCmp(
+ null, "test", 1, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_ID_FROM_TEXT_SEARCH, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID)));
+ assertCursorColumns(cursor, MEDIA_PROJECTIONS);
+ }
+
+ @Test
+ public void testFetchMediasInMediaSetFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchMediasInMediaSetFromCmp(TEST_MEDIA_SET_ID,
+ null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_ID_IN_MEDIA_SET, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID)));
+ assertCursorColumns(cursor, MEDIA_PROJECTIONS);
+ }
+
+
+ @Test
+ public void testFetchMediaCategoriesFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchMediaCategoriesFromCmp(null,
+ null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_CATEGORY_ID, cursor.getString(
+ cursor.getColumnIndex(CloudMediaProviderContract.MediaCategoryColumns.ID)));
+ assertCursorColumns(cursor, CloudMediaProviderContract.MediaCategoryColumns.ALL_PROJECTION);
+ }
+
+ @Test
+ public void testFetchMediaSetsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchMediaSetsFromCmp(TEST_MEDIA_CATEGORY_ID,
+ null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_SET_ID, cursor.getString(
+ cursor.getColumnIndex(CloudMediaProviderContract.MediaSetColumns.ID)));
+ assertCursorColumns(cursor, CloudMediaProviderContract.MediaSetColumns.ALL_PROJECTION);
+ }
+
+ private static void assertCursorColumns(Cursor cursor, String[] projections) {
+ for (String columnName : projections) {
+ assertTrue(cursor.getColumnIndex(columnName) >= 0);
+ }
+ }
+
+}