• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.provider;
18 
19 import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER;
20 import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
21 import static android.provider.CloudMediaProviderContract.EXTRA_ERROR_MESSAGE;
22 import static android.provider.CloudMediaProviderContract.EXTRA_FILE_DESCRIPTOR;
23 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
24 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB;
25 import static android.provider.CloudMediaProviderContract.EXTRA_PROVIDER_CAPABILITIES;
26 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
27 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
28 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK;
29 import static android.provider.CloudMediaProviderContract.KEY_MEDIA_CATEGORY_ID;
30 import static android.provider.CloudMediaProviderContract.KEY_MEDIA_SET_ID;
31 import static android.provider.CloudMediaProviderContract.KEY_PARENT_CATEGORY_ID;
32 import static android.provider.CloudMediaProviderContract.KEY_PREFIX_TEXT;
33 import static android.provider.CloudMediaProviderContract.KEY_SEARCH_TEXT;
34 import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
35 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER;
36 import static android.provider.CloudMediaProviderContract.METHOD_GET_CAPABILITIES;
37 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
38 import static android.provider.CloudMediaProviderContract.URI_PATH_ALBUM;
39 import static android.provider.CloudMediaProviderContract.URI_PATH_DELETED_MEDIA;
40 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA;
41 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY;
42 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO;
43 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_SET;
44 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET;
45 import static android.provider.CloudMediaProviderContract.URI_PATH_SEARCH_MEDIA;
46 import static android.provider.CloudMediaProviderContract.URI_PATH_SEARCH_SUGGESTION;
47 import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER;
48 
49 import android.annotation.DurationMillisLong;
50 import android.annotation.FlaggedApi;
51 import android.annotation.IntDef;
52 import android.annotation.NonNull;
53 import android.annotation.Nullable;
54 import android.annotation.SuppressLint;
55 import android.content.ContentProvider;
56 import android.content.ContentResolver;
57 import android.content.ContentValues;
58 import android.content.Context;
59 import android.content.UriMatcher;
60 import android.content.pm.ProviderInfo;
61 import android.content.res.AssetFileDescriptor;
62 import android.database.Cursor;
63 import android.graphics.PixelFormat;
64 import android.graphics.Point;
65 import android.media.MediaPlayer;
66 import android.net.Uri;
67 import android.os.Binder;
68 import android.os.Bundle;
69 import android.os.CancellationSignal;
70 import android.os.IBinder;
71 import android.os.ParcelFileDescriptor;
72 import android.os.RemoteCallback;
73 import android.util.DisplayMetrics;
74 import android.util.Log;
75 import android.view.Surface;
76 import android.view.SurfaceHolder;
77 
78 import com.android.providers.media.flags.Flags;
79 
80 import java.io.FileNotFoundException;
81 import java.lang.annotation.Retention;
82 import java.lang.annotation.RetentionPolicy;
83 import java.util.Objects;
84 
85 /**
86  * Base class for a cloud media provider. A cloud media provider offers read-only access to durable
87  * media files, specifically photos and videos stored on a local disk, or files in a cloud storage
88  * service. To create a cloud media provider, extend this class, implement the abstract methods,
89  * and add it to your manifest like this:
90  *
91  * <pre class="prettyprint">&lt;manifest&gt;
92  *    ...
93  *    &lt;application&gt;
94  *        ...
95  *        &lt;provider
96  *            android:name="com.example.MyCloudProvider"
97  *            android:authorities="com.example.mycloudprovider"
98  *            android:exported="true"
99  *            android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
100  *            &lt;intent-filter&gt;
101  *                &lt;action android:name="android.content.action.CLOUD_MEDIA_PROVIDER" /&gt;
102  *            &lt;/intent-filter&gt;
103  *        &lt;/provider&gt;
104  *        ...
105  *    &lt;/application&gt;
106  *&lt;/manifest&gt;</pre>
107  * <p>
108  * When defining your provider, you must protect it with the
109  * {@link CloudMediaProviderContract#MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION}, which is a permission
110  * only the system can obtain, trying to define an unprotected {@link CloudMediaProvider} will
111  * result in a {@link SecurityException}.
112  * <p>
113  * Applications cannot use a cloud media provider directly; they must go through
114  * {@link MediaStore#ACTION_PICK_IMAGES} which requires a user to actively navigate and select
115  * media items. When a user selects a media item through that UI, the system issues narrow URI
116  * permission grants to the requesting application.
117  * <h3>Media items</h3>
118  * <p>
119  * A media item must be an openable stream (with a specific MIME type). Media items can belong to
120  * zero or more albums. Albums cannot contain other albums.
121  * <p>
122  * Each item under a provider is uniquely referenced by its media or album id, which must not
123  * change which must be unique across all collection IDs as returned by
124  * {@link #onGetMediaCollectionInfo}.
125  *
126  * @see MediaStore#ACTION_PICK_IMAGES
127  */
128 public abstract class CloudMediaProvider extends ContentProvider {
129     private static final String TAG = "CloudMediaProvider";
130 
131     private static final int MATCH_MEDIAS = 1;
132     private static final int MATCH_DELETED_MEDIAS = 2;
133     private static final int MATCH_ALBUMS = 3;
134     private static final int MATCH_MEDIA_COLLECTION_INFO = 4;
135     private static final int MATCH_SURFACE_CONTROLLER = 5;
136     private static final int MATCH_MEDIA_CATEGORIES = 6;
137     private static final int MATCH_MEDIA_SETS = 7;
138     private static final int MATCH_SEARCH_SUGGESTION = 8;
139     private static final int MATCH_SEARCH = 9;
140     private static final int MATCH_MEDIAS_IN_MEDIA_SET = 10;
141 
142     private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true;
143     private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false;
144 
145     private final UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
146     private volatile int mMediaStoreAuthorityAppId;
147 
148     private String mAuthority;
149 
150 
151     /**
152      * Implementation is provided by the parent class. Cannot be overridden.
153      */
154     @Override
attachInfo(@onNull Context context, @NonNull ProviderInfo info)155     public final void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
156         registerAuthority(info.authority);
157 
158         super.attachInfo(context, info);
159     }
160 
registerAuthority(String authority)161     private void registerAuthority(String authority) {
162         mAuthority = authority;
163         mMatcher.addURI(authority, URI_PATH_MEDIA, MATCH_MEDIAS);
164         mMatcher.addURI(authority, URI_PATH_DELETED_MEDIA, MATCH_DELETED_MEDIAS);
165         mMatcher.addURI(authority, URI_PATH_ALBUM, MATCH_ALBUMS);
166         mMatcher.addURI(authority, URI_PATH_MEDIA_COLLECTION_INFO, MATCH_MEDIA_COLLECTION_INFO);
167         mMatcher.addURI(authority, URI_PATH_SURFACE_CONTROLLER, MATCH_SURFACE_CONTROLLER);
168         mMatcher.addURI(authority, URI_PATH_MEDIA_CATEGORY, MATCH_MEDIA_CATEGORIES);
169         mMatcher.addURI(authority, URI_PATH_MEDIA_SET, MATCH_MEDIA_SETS);
170         mMatcher.addURI(authority, URI_PATH_SEARCH_SUGGESTION, MATCH_SEARCH_SUGGESTION);
171         mMatcher.addURI(authority, URI_PATH_SEARCH_MEDIA, MATCH_SEARCH);
172         mMatcher.addURI(authority, URI_PATH_MEDIA_IN_MEDIA_SET, MATCH_MEDIAS_IN_MEDIA_SET);
173     }
174 
175     /**
176      * Returns {@link Bundle} containing binder to {@link IAsyncContentProvider}.
177      *
178      * @hide
179      */
180     @NonNull
onGetAsyncContentProvider()181     public final Bundle onGetAsyncContentProvider() {
182         Bundle bundle = new Bundle();
183         bundle.putBinder(EXTRA_ASYNC_CONTENT_PROVIDER,
184                 (new AsyncContentProviderWrapper()).asBinder());
185         return bundle;
186     }
187 
188     /**
189      * Returns the {@link CloudMediaProviderContract.Capabilities} of this
190      * CloudMediaProvider.
191      *
192      * This object is used to determine which APIs can be safely invoked during
193      * runtime.
194      *
195      * If not overridden the default capabilities are used.
196      *
197      * IMPORTANT: This method is performance critical and should avoid long running
198      * or expensive operations.
199      *
200      * @see CloudMediaProviderContract.Capabilities
201      *
202      */
203     @NonNull
204     @FlaggedApi(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES)
onGetCapabilities()205     public CloudMediaProviderContract.Capabilities onGetCapabilities() {
206         return new CloudMediaProviderContract.Capabilities.Builder().build();
207     }
208 
209     /**
210      * Returns metadata about the media collection itself.
211      * <p>
212      * This is useful for the OS to determine if its cache of media items in the collection is
213      * still valid and if a full or incremental sync is required with {@link #onQueryMedia}.
214      * <p>
215      * This method might be called by the OS frequently and is performance critical, hence it should
216      * avoid long running operations.
217      * <p>
218      * If the provider handled any filters in {@code extras}, it must add the key to the
219      * {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned {@link Bundle}.
220      *
221      * @param extras containing keys to filter result:
222      * <ul>
223      * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
224      * </ul>
225      *
226      * @return {@link Bundle} containing {@link CloudMediaProviderContract.MediaCollectionInfo}
227      * <ul>
228      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}
229      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}
230      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}
231      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}
232      * </ul>
233      */
234     @SuppressWarnings("unused")
235     @NonNull
onGetMediaCollectionInfo(@onNull Bundle extras)236     public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
237 
238     /**
239      * Returns a cursor representing all media items in the media collection optionally filtered by
240      * {@code extras} and sorted in reverse chronological order of
241      * {@link CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS}, i.e. most recent items
242      * first.
243      * <p>
244      * The cloud media provider must set the
245      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
246      * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
247      * returned {@link Cursor}.
248      * <p>
249      * If the cloud media provider handled any filters in {@code extras}, it must add the key to
250      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
251      * {@link Cursor#setExtras} {@link Bundle}.
252      *
253      * @param extras containing keys to filter media items:
254      * <ul>
255      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
256      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
257      * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
258      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
259      * </ul>
260      * @return cursor representing media items containing all
261      * {@link CloudMediaProviderContract.MediaColumns} columns
262      */
263     @SuppressWarnings("unused")
264     @NonNull
onQueryMedia(@onNull Bundle extras)265     public abstract Cursor onQueryMedia(@NonNull Bundle extras);
266 
267     /**
268      * Returns a {@link Cursor} representing all deleted media items in the entire media collection
269      * within the current provider version as returned by {@link #onGetMediaCollectionInfo}. These
270      * items can be optionally filtered by {@code extras}.
271      * <p>
272      * The cloud media provider must set the
273      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
274      * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
275      * returned {@link Cursor}.
276      * <p>
277      * If the provider handled any filters in {@code extras}, it must add the key to
278      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
279      * {@link Cursor#setExtras} {@link Bundle}.
280      *
281      * @param extras containing keys to filter deleted media items:
282      * <ul>
283      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
284      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
285      * </ul>
286      * @return cursor representing deleted media items containing just the
287      * {@link CloudMediaProviderContract.MediaColumns#ID} column
288      */
289     @SuppressWarnings("unused")
290     @NonNull
onQueryDeletedMedia(@onNull Bundle extras)291     public abstract Cursor onQueryDeletedMedia(@NonNull Bundle extras);
292 
293     /**
294      * Returns a cursor representing all album items in the media collection optionally filtered
295      * by {@code extras} and sorted in reverse chronological order of
296      * {@link CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}, i.e. most recent items
297      * first.
298      * <p>
299      * The cloud media provider must set the
300      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
301      * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
302      * returned {@link Cursor}.
303      *
304      * <p>
305      * If the provider handled any filters in {@code extras}, it must add the key to
306      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
307      * {@link Cursor#setExtras} {@link Bundle}.
308      *
309      * @param extras containing keys to filter album items:
310      * <ul>
311      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
312      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
313      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
314      * <li> {@link android.content.Intent#EXTRA_MIME_TYPES}
315      * </ul>
316      * @return cursor representing album items containing all
317      * {@link CloudMediaProviderContract.AlbumColumns} columns
318      */
319     @SuppressWarnings("unused")
320     @NonNull
onQueryAlbums(@onNull Bundle extras)321     public Cursor onQueryAlbums(@NonNull Bundle extras) {
322         throw new UnsupportedOperationException("queryAlbums not supported");
323     }
324 
325     /**
326      * Queries for the available MediaCategories under the given {@code parentCategoryId},
327      * filtered by {@code extras}. The columns of MediaCategories are
328      * in the class {@link CloudMediaProviderContract.MediaCategoryColumns}.
329      *
330      * <p>
331      * When {@code parentCategoryId} is null, this returns the root categories.
332      *
333      * <p>
334      * The order in which media categories are sorted in the cursor
335      * will be retained when displaying results to the user.
336      *
337      * <p>
338      * The cloud media provider must set the
339      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor
340      * by using {@link Cursor#setExtras}. Not setting this is an error and invalidates the
341      * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation.
342      *
343      * <p>
344      * {@code extras} may contain some key-value pairs which should be used to filter the results.
345      * If the provider handled any filters in {@code extras}, it must add the key to
346      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using
347      * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is
348      * without the extra being used.
349      * Note: Currently this function does not pass any params in {@code extras}.
350      *
351      * @param parentCategoryId   the ID of the parent category to filter media categories.
352      * @param extras             containing keys to filter media categories:
353      *                           <ul>
354      *                           <li> {@link android.content.Intent#EXTRA_MIME_TYPES}
355      *                           </ul>
356      * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled.
357      * @return cursor with {@link CloudMediaProviderContract.MediaCategoryColumns} columns
358      */
359     @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
360     @NonNull
onQueryMediaCategories(@ullable String parentCategoryId, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)361     public Cursor onQueryMediaCategories(@Nullable String parentCategoryId,
362             @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) {
363         throw new UnsupportedOperationException("onQueryMediaCategories is not supported");
364     }
365 
366     /**
367      * Queries for the available MediaSets under a given {@code mediaCategoryId},
368      * filtered by {@code extras}. The columns of MediaSet are in the class
369      * {@link CloudMediaProviderContract.MediaSetColumns}.
370      *
371      * <p>
372      * This returns MediaSets directly inside the given MediaCategoryId.
373      * If the passed mediaCategoryId has some more nested mediaCategories, the mediaSets inside
374      * the nested mediaCategories must not be returned in this response.
375      *
376      * <p>
377      * The order in which media sets are sorted in the cursor
378      * will be retained when displaying results to the user.
379      *
380      * <p>
381      * The cloud media provider must set the
382      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor
383      * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the
384      * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation.
385      *
386      * <p>
387      * {@code extras} may contain some key-value pairs which should be used to prepare the results.
388      * If the provider handled any filters in {@code extras}, it must add the key to
389      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using
390      * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is
391      * without the extra being used.
392      *
393      * <p>
394      * If the cloud media provider supports pagination, they can set
395      * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token,
396      * as part of the returned cursor by using {@link Cursor#setExtras}.
397      * If a token is set, the OS will pass it as a  key-value pair in {@code extras}
398      * when querying for query media sets for subsequent pages.
399      * The provider can keep returning pagination tokens in the returned cursor
400      * by using {@link Cursor#setExtras} until the last page at which point it should not
401      * set a token in the returned cursor.
402      *
403      * @param mediaCategoryId    the ID of the media category to filter media sets.
404      * @param extras             containing keys to filter media sets:
405      *                           <ul>
406      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
407      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
408      *                           <li> {@link android.content.Intent#EXTRA_MIME_TYPES}
409      *                           </ul>
410      * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled.
411      * @return cursor representing {@link CloudMediaProviderContract.MediaSetColumns} columns
412      */
413     @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
414     @NonNull
onQueryMediaSets(@onNull String mediaCategoryId, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)415     public Cursor onQueryMediaSets(@NonNull String mediaCategoryId,
416             @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) {
417         throw new UnsupportedOperationException("onQueryMediaSets is not supported");
418     }
419 
420     /**
421      * Queries for the available SearchSuggestions based on a {@code prefixText},
422      * filtered by {@code extras}. The columns of SearchSuggestions are in the class
423      * {@link CloudMediaProviderContract.SearchSuggestionColumns}
424      *
425      * <p>
426      * If the user has not started typing, this is considered as zero state suggestion.
427      * In this case {@code prefixText} will be empty string.
428      *
429      * <p>
430      * The order in which suggestions are sorted in the cursor
431      * will be retained when displaying results to the user.
432      *
433      * <p>
434      * The cloud media provider must set the
435      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor
436      * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the
437      * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation.
438      *
439      * <p>
440      * {@code extras} may contain some key-value pairs which should be used to prepare
441      * the results.
442      * If the provider handled any params in {@code extras}, it must add the key to
443      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using
444      * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is
445      * without the extra being used.
446      * Note: Currently this function does not pass any key-value params in {@code extras}.
447      *
448      * <p>
449      * Results may not be displayed if it takes longer than 300 milliseconds to get a response from
450      * the cloud media provider.
451      *
452      * @param prefixText         the prefix text to filter search suggestions.
453      * @param extras             containing keys to filter search suggestions.
454      *                           <ul>
455      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
456      *                           </ul>
457      * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled.
458      * @return cursor representing search suggestions containing all
459      * {@see CloudMediaProviderContract.SearchSuggestionColumns} columns
460      */
461     @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
462     @NonNull
onQuerySearchSuggestions(@onNull String prefixText, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)463     public Cursor onQuerySearchSuggestions(@NonNull String prefixText,
464             @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) {
465         throw new UnsupportedOperationException("onQuerySearchSuggestions is not supported");
466     }
467 
468     /**
469      * Queries for the available media items under a given {@code mediaSetId},
470      * filtered by {@code extras}. The columns of Media are in the class
471      * {@link CloudMediaProviderContract.MediaColumns}. {@code mediaSetId} is the ID given
472      * as part of {@link CloudMediaProviderContract.MediaSetColumns#ID}
473      *
474      * <p>
475      * The order in which media items are sorted in the cursor
476      * will be retained when displaying results to the user.
477      *
478      * <p>
479      * The cloud media provider must set the
480      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
481      * {@link Cursor} by using {@link Cursor#setExtras}.
482      * Not setting this is an error and invalidates the
483      * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation.
484      *
485      * <p>
486      * {@code extras} may contain some key-value pairs which should be used to prepare the results.
487      * If the provider handled any filters in {@code extras}, it must add the key to
488      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using
489      * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is
490      * without the extra being used.
491      *
492      * <p>
493      * If the cloud media provider supports pagination, they can set
494      * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token,
495      * as part of the returned cursor by using {@link Cursor#setExtras}.
496      * If a token is set, the OS will pass it as a  key-value pair in {@code extras}
497      * when querying for media for subsequent pages.
498      * The provider can keep returning pagination tokens in the returned cursor
499      * by using {@link Cursor#setExtras} until the last page at which point it should not
500      * set a token in the returned cursor.
501      *
502      * @param mediaSetId         the ID of the media set to filter media items.
503      * @param extras             containing keys to filter media items:
504      *                           <ul>
505      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
506      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
507      *                           <li> {@link CloudMediaProviderContract#EXTRA_SORT_ORDER}
508      *                           <li> {@link android.content.Intent#EXTRA_MIME_TYPES}
509      *                           </ul>
510      * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled.
511      * @return cursor representing {@link CloudMediaProviderContract.MediaColumns} columns
512      */
513     @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
514     @NonNull
onQueryMediaInMediaSet(@onNull String mediaSetId, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)515     public Cursor onQueryMediaInMediaSet(@NonNull String mediaSetId,
516             @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) {
517         throw new UnsupportedOperationException("onQueryMediaInMediaSet is not supported");
518     }
519 
520     /**
521      * Searches media items based on a selected suggestion, managed by {@code extras} and
522      * returns a cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match.
523      *
524      * <p>
525      * The cloud media provider must set the
526      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor
527      * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the
528      * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation.
529      * <p>
530      *
531      * {@code extras} may contain some key-value pairs which should be used to prepare the results.
532      * If the provider handled any params in {@code extras}, it must add the key to
533      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using
534      * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is
535      * without the extra being used.
536      *
537      * <p>
538      * An example user journey:
539      * <ol>
540      *     <li>User enters the search prompt.</li>
541      *     <li>Using {@link #onQuerySearchSuggestions}, photo picker display suggestions as the user
542      *     keeps typing.</li>
543      *     <li>User selects a suggestion, Photo picker calls:
544      *     {@code onSearchMedia(suggestedMediaSetId, fallbackSearchText, extras)}
545      *     with the {@code suggestedMediaSetId} corresponding to the user chosen suggestion.
546      *     {@link CloudMediaProviderContract.SearchSuggestionColumns#MEDIA_SET_ID}</li>
547      * </ol>
548      *
549      *
550      * <p>
551      * If the cloud media provider supports pagination, they can set
552      * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token,
553      * as part of the returned cursor by using {@link Cursor#setExtras}.
554      * If a token is set, the OS will pass it as a key value pair in {@code extras}
555      * when querying for search media for subsequent pages.
556      * The provider can keep returning pagination tokens in the returned cursor
557      * by using {@link Cursor#setExtras} until the last page at which point it should not
558      * set a token in the returned cursor
559      *
560      * <p>
561      * Results may not be displayed if it takes longer than 3 seconds to get a paged response from
562      * the cloud media provider.
563      *
564      * @param suggestedMediaSetId the media set ID of the suggestion that the user wants to search.
565      * @param fallbackSearchText  optional search text to be used when {@code suggestedMediaSetId}
566      *                            is not useful.
567      * @param extras              containing keys to manage the search results:
568      *                            <ul>
569      *                            <li>{@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
570      *                            <li>{@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
571      *                            <li>{@link CloudMediaProviderContract#EXTRA_SORT_ORDER}
572      *                            <li> {@link android.content.Intent#EXTRA_MIME_TYPES}
573      *                            </ul>
574      * @param cancellationSignal  {@link CancellationSignal} to check if request has been cancelled.
575      * @return cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match.
576      * @see CloudMediaProviderContract.SearchSuggestionColumns#MEDIA_SET_ID
577      */
578     @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
579     @NonNull
onSearchMedia(@onNull String suggestedMediaSetId, @Nullable String fallbackSearchText, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)580     public Cursor onSearchMedia(@NonNull String suggestedMediaSetId,
581             @Nullable String fallbackSearchText,
582             @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) {
583         throw new UnsupportedOperationException("onSearchMedia for"
584                 + " suggestion media set id is not supported");
585     }
586 
587     /**
588      * Searches media items based on entered search text, managed by {@code extras} and
589      * returns a cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match.
590      *
591      * <p>
592      * The cloud media provider must set the
593      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor
594      * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the
595      * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation.
596      *
597      * <p>
598      * {@code extras} may contain some key-value pairs which should be used to prepare the results.
599      * If the provider handled any params in {@code extras}, it must add the key to
600      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using
601      * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is
602      * without the extra being used.
603      *
604      * <p>
605      * An example user journey:
606      * <ol>
607      *     <li>User enters the search prompt.</li>
608      *     <li>Using {@link #onQuerySearchSuggestions}, photo picker display suggestions as the user
609      *     keeps typing.</li>
610      *     <li>User types completely and then enters search,
611      *     Photo picker calls: {@code onSearchMedia(searchText, extras)}</li>
612      * </ol>
613      *
614      * <p>
615      * If the cloud media provider supports pagination, they can set
616      * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token,
617      * as part of the returned cursor by using {@link Cursor#setExtras}.
618      * If a token is set, the OS will pass it as a key value pair in {@code extras}
619      * when querying for search media for subsequent pages.
620      * The provider can keep returning pagination tokens in the returned cursor
621      * by using {@link Cursor#setExtras} until the last page at which point it should not
622      * set a token in the returned cursor.
623      *
624      * <p>
625      * Results may not be displayed if it takes longer than 3 seconds to get a paged response from
626      * the cloud media provider.
627      *
628      * @param searchText         search text to be used.
629      * @param extras             containing keys to manage the search results:
630      *                           <ul>
631      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
632      *                           <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
633      *                           <li> {@link CloudMediaProviderContract#EXTRA_SORT_ORDER}
634      *                           <li> {@link android.content.Intent#EXTRA_MIME_TYPES}
635      *                           </ul>
636      * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled.
637      * @return cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match.
638      */
639     @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
640     @NonNull
onSearchMedia(@onNull String searchText, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)641     public Cursor onSearchMedia(@NonNull String searchText,
642             @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) {
643         throw new UnsupportedOperationException("onSearchMedia for search text is not supported");
644     }
645 
646     /**
647      * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId}
648      * <p>The cloud media provider should strictly return thumbnail in the original
649      * {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item.
650      * <p>
651      * This is expected to be a much lower resolution version than the item returned by
652      * {@link #onOpenMedia}.
653      * <p>
654      * If you block while downloading content, you should periodically check
655      * {@link CancellationSignal#isCanceled()} to abort abandoned open requests.
656      *
657      * @param mediaId the media item to return
658      * @param size the dimensions of the thumbnail to return. The returned file descriptor doesn't
659      * have to match the {@code size} precisely because the OS will adjust the dimensions before
660      * usage. Implementations can return close approximations especially if the approximation is
661      * already locally on the device and doesn't require downloading from the cloud.
662      * @param extras to modify the way the fd is opened, e.g. for video files we may request a
663      * thumbnail image instead of a video with
664      * {@link CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL}
665      * @param signal used by the OS to signal if the request should be cancelled
666      * @return read-only file descriptor for accessing the thumbnail for the media file
667      *
668      * @see #onOpenMedia
669      * @see CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL
670      */
671     @SuppressWarnings("unused")
672     @NonNull
onOpenPreview(@onNull String mediaId, @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal)673     public abstract AssetFileDescriptor onOpenPreview(@NonNull String mediaId,
674             @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal)
675             throws FileNotFoundException;
676 
677     /**
678      * Returns the full size media item identified by {@code mediaId}.
679      * <p>
680      * If you block while downloading content, you should periodically check
681      * {@link CancellationSignal#isCanceled()} to abort abandoned open requests.
682      *
683      * @param mediaId the media item to return
684      * @param extras to modify the way the fd is opened, there's none at the moment, but some
685      * might be implemented in the future
686      * @param signal used by the OS to signal if the request should be cancelled
687      * @return read-only file descriptor for accessing the media file
688      *
689      * @see #onOpenPreview
690      */
691     @SuppressWarnings("unused")
692     @NonNull
onOpenMedia(@onNull String mediaId, @Nullable Bundle extras, @Nullable CancellationSignal signal)693     public abstract ParcelFileDescriptor onOpenMedia(@NonNull String mediaId,
694             @Nullable Bundle extras, @Nullable CancellationSignal signal)
695             throws FileNotFoundException;
696 
697     /**
698      * Returns a {@link CloudMediaSurfaceController} used for rendering the preview of media items,
699      * or null if preview rendering is not supported.
700      *
701      * @param config containing configuration parameters for {@link CloudMediaSurfaceController}
702      * <ul>
703      * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
704      * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
705      * </ul>
706      * @param callback {@link CloudMediaSurfaceStateChangedCallback} to send state updates for
707      *                 {@link Surface} to picker launched via {@link MediaStore#ACTION_PICK_IMAGES}
708      */
709     @Nullable
onCreateCloudMediaSurfaceController(@onNull Bundle config, @NonNull CloudMediaSurfaceStateChangedCallback callback)710     public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config,
711             @NonNull CloudMediaSurfaceStateChangedCallback callback) {
712         return null;
713     }
714 
715     /**
716      * Implementation is provided by the parent class. Cannot be overridden.
717      */
718     @Override
719     @NonNull
call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)720     public final Bundle call(@NonNull String method, @Nullable String arg,
721             @Nullable Bundle extras) {
722         if (!method.startsWith("android:")) {
723             // Ignore non-platform methods
724             return super.call(method, arg, extras);
725         }
726 
727         try {
728             return callUnchecked(method, arg, extras);
729         } catch (FileNotFoundException e) {
730             throw new RuntimeException(e);
731         }
732     }
733 
callUnchecked(@onNull String method, @Nullable String arg, @Nullable Bundle extras)734     private Bundle callUnchecked(@NonNull String method, @Nullable String arg,
735             @Nullable Bundle extras)
736             throws FileNotFoundException {
737         if (extras == null) {
738             extras = new Bundle();
739         }
740         Bundle result = new Bundle();
741         if (METHOD_GET_MEDIA_COLLECTION_INFO.equals(method)) {
742             long startTime = System.currentTimeMillis();
743             result = onGetMediaCollectionInfo(extras);
744             CmpApiVerifier.verifyApiResult(new CmpApiResult(
745                     CmpApiVerifier.CloudMediaProviderApis.OnGetMediaCollectionInfo, result),
746                     System.currentTimeMillis() - startTime, mAuthority);
747         } else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) {
748             result = onCreateCloudMediaSurfaceController(extras);
749         } else if (METHOD_GET_ASYNC_CONTENT_PROVIDER.equals(method)) {
750             result = onGetAsyncContentProvider();
751         } else if (Flags.enableCloudMediaProviderCapabilities()
752                     && METHOD_GET_CAPABILITIES.equals(method)) {
753             long startTime = System.currentTimeMillis();
754 
755             CloudMediaProviderContract.Capabilities capabilities = onGetCapabilities();
756             result.putParcelable(EXTRA_PROVIDER_CAPABILITIES, capabilities);
757 
758             CmpApiVerifier.verifyApiResult(new CmpApiResult(
759                     CmpApiVerifier.CloudMediaProviderApis.OnGetCapabilities, result),
760                     System.currentTimeMillis() - startTime, mAuthority);
761         } else {
762             throw new UnsupportedOperationException("Method not supported " + method);
763         }
764         return result;
765     }
766 
onCreateCloudMediaSurfaceController(@onNull Bundle extras)767     private Bundle onCreateCloudMediaSurfaceController(@NonNull Bundle extras) {
768         Objects.requireNonNull(extras);
769 
770         final IBinder binder = extras.getBinder(EXTRA_SURFACE_STATE_CALLBACK);
771         if (binder == null) {
772             throw new IllegalArgumentException("Missing surface state callback");
773         }
774 
775         final boolean enableLoop = extras.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
776                 DEFAULT_LOOPING_PLAYBACK_ENABLED);
777         final boolean muteAudio = extras.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
778                 DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
779         final String authority = extras.getString(EXTRA_AUTHORITY);
780         final CloudMediaSurfaceStateChangedCallback callback =
781                 new CloudMediaSurfaceStateChangedCallback(
782                         ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder));
783         final Bundle config = new Bundle();
784         config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, enableLoop);
785         config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, muteAudio);
786         config.putString(EXTRA_AUTHORITY, authority);
787         final CloudMediaSurfaceController controller =
788                 onCreateCloudMediaSurfaceController(config, callback);
789         if (controller == null) {
790             Log.d(TAG, "onCreateCloudMediaSurfaceController returned null");
791             return Bundle.EMPTY;
792         }
793 
794         Bundle result = new Bundle();
795         result.putBinder(EXTRA_SURFACE_CONTROLLER,
796                 new CloudMediaSurfaceControllerWrapper(controller).asBinder());
797         return result;
798     }
799 
800     /**
801      * Implementation is provided by the parent class. Cannot be overridden.
802      *
803      * @see #onOpenMedia
804      */
805     @NonNull
806     @Override
openFile(@onNull Uri uri, @NonNull String mode)807     public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
808             throws FileNotFoundException {
809         return openFile(uri, mode, null);
810     }
811 
812     /**
813      * Implementation is provided by the parent class. Cannot be overridden.
814      *
815      * @see #onOpenMedia
816      */
817     @NonNull
818     @Override
openFile(@onNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal)819     public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode,
820             @Nullable CancellationSignal signal) throws FileNotFoundException {
821         String mediaId = uri.getLastPathSegment();
822 
823         long startTime = System.currentTimeMillis();
824         ParcelFileDescriptor result = onOpenMedia(mediaId, /* extras */ null, signal);
825         CmpApiVerifier.verifyApiResult(new CmpApiResult(
826                         CmpApiVerifier.CloudMediaProviderApis.OnOpenMedia, result),
827                 System.currentTimeMillis() - startTime, mAuthority);
828         return result;
829     }
830 
831     /**
832      * Implementation is provided by the parent class. Cannot be overridden.
833      *
834      * @see #onOpenPreview
835      * @see #onOpenMedia
836      */
837     @NonNull
838     @Override
openTypedAssetFile(@onNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts)839     public final AssetFileDescriptor openTypedAssetFile(@NonNull Uri uri,
840             @NonNull String mimeTypeFilter, @Nullable Bundle opts) throws FileNotFoundException {
841         return openTypedAssetFile(uri, mimeTypeFilter, opts, null);
842     }
843 
844     /**
845      * Implementation is provided by the parent class. Cannot be overridden.
846      *
847      * @see #onOpenPreview
848      * @see #onOpenMedia
849      */
850     @NonNull
851     @Override
openTypedAssetFile( @onNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts, @Nullable CancellationSignal signal)852     public final AssetFileDescriptor openTypedAssetFile(
853             @NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts,
854             @Nullable CancellationSignal signal) throws FileNotFoundException {
855         final String mediaId = uri.getLastPathSegment();
856         final Bundle bundle = new Bundle();
857         Point previewSize = null;
858 
859         final DisplayMetrics screenMetrics = getContext().getResources().getDisplayMetrics();
860         int minPreviewLength = Math.min(screenMetrics.widthPixels, screenMetrics.heightPixels);
861 
862         if (opts != null) {
863             bundle.putBoolean(EXTRA_MEDIASTORE_THUMB, opts.getBoolean(EXTRA_MEDIASTORE_THUMB));
864 
865             if (opts.containsKey(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)) {
866                 bundle.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true);
867                 minPreviewLength = minPreviewLength / 2;
868             }
869 
870             previewSize = opts.getParcelable(ContentResolver.EXTRA_SIZE);
871         }
872 
873         if (previewSize == null) {
874             previewSize = new Point(minPreviewLength, minPreviewLength);
875         }
876 
877         long startTime = System.currentTimeMillis();
878         AssetFileDescriptor result = onOpenPreview(mediaId, previewSize, bundle, signal);
879         CmpApiVerifier.verifyApiResult(new CmpApiResult(
880                         CmpApiVerifier.CloudMediaProviderApis.OnOpenPreview, result, previewSize),
881                 System.currentTimeMillis() - startTime, mAuthority);
882         return result;
883     }
884 
885     /**
886      * Implementation is provided by the parent class. Cannot be overridden.
887      *
888      * @see #onQueryMedia
889      * @see #onQueryDeletedMedia
890      * @see #onQueryAlbums
891      */
892     @NonNull
893     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)894     public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
895             @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
896         if (queryArgs == null) {
897             queryArgs = new Bundle();
898         }
899         Cursor result;
900         long startTime = System.currentTimeMillis();
901         switch (mMatcher.match(uri)) {
902             case MATCH_MEDIAS:
903                 result = onQueryMedia(queryArgs);
904                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
905                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryMedia, result),
906                         System.currentTimeMillis() - startTime, mAuthority);
907                 break;
908             case MATCH_DELETED_MEDIAS:
909                 result = onQueryDeletedMedia(queryArgs);
910                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
911                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryDeletedMedia, result),
912                         System.currentTimeMillis() - startTime, mAuthority);
913                 break;
914             case MATCH_ALBUMS:
915                 result = onQueryAlbums(queryArgs);
916                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
917                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryAlbums, result),
918                         System.currentTimeMillis() - startTime, mAuthority);
919                 break;
920             case MATCH_MEDIA_CATEGORIES:
921                 final String parentCategoryId = queryArgs.getString(KEY_PARENT_CATEGORY_ID);
922                 queryArgs.remove(KEY_PARENT_CATEGORY_ID);
923                 result = onQueryMediaCategories(parentCategoryId, queryArgs, cancellationSignal
924                 );
925                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
926                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaCategories,
927                                 result),
928                         System.currentTimeMillis() - startTime, mAuthority);
929                 break;
930             case MATCH_MEDIA_SETS:
931                 final String mediaCategoryId = queryArgs.getString(KEY_MEDIA_CATEGORY_ID);
932                 queryArgs.remove(KEY_MEDIA_CATEGORY_ID);
933                 result = onQueryMediaSets(mediaCategoryId, queryArgs, cancellationSignal);
934                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
935                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaSets,
936                                 result),
937                         System.currentTimeMillis() - startTime, mAuthority);
938                 break;
939             case MATCH_SEARCH_SUGGESTION:
940                 final String prefixText = queryArgs.getString(KEY_PREFIX_TEXT);
941                 queryArgs.remove(KEY_PREFIX_TEXT);
942                 result = onQuerySearchSuggestions(prefixText, queryArgs, cancellationSignal);
943                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
944                                 CmpApiVerifier.CloudMediaProviderApis.OnQuerySearchSuggestions,
945                                 result),
946                         System.currentTimeMillis() - startTime, mAuthority);
947                 break;
948             case MATCH_MEDIAS_IN_MEDIA_SET:
949                 final String mediaSetId = queryArgs.getString(KEY_MEDIA_SET_ID);
950                 queryArgs.remove(KEY_MEDIA_SET_ID);
951                 result = onQueryMediaInMediaSet(mediaSetId, queryArgs, cancellationSignal);
952                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
953                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaInMediaSet,
954                                 result),
955                         System.currentTimeMillis() - startTime, mAuthority);
956                 break;
957             case MATCH_SEARCH:
958                 final String searchText = queryArgs.getString(KEY_SEARCH_TEXT);
959                 queryArgs.remove(KEY_SEARCH_TEXT);
960                 final String searchMediaSetId = queryArgs.getString(KEY_MEDIA_SET_ID);
961                 queryArgs.remove(KEY_MEDIA_SET_ID);
962                 if (searchMediaSetId != null) {
963                     result = onSearchMedia(
964                             searchMediaSetId, searchText, queryArgs, cancellationSignal);
965                 } else if (searchText != null) {
966                     result = onSearchMedia(searchText, queryArgs, cancellationSignal);
967                 } else {
968                     throw new IllegalArgumentException("both suggested media set id "
969                             + "and search text can not be null together");
970                 }
971                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
972                                 CmpApiVerifier.CloudMediaProviderApis.OnSearchMedia,
973                                 result),
974                         System.currentTimeMillis() - startTime, mAuthority);
975                 break;
976             default:
977                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
978         }
979         return result;
980     }
981 
982     /**
983      * Implementation is provided by the parent class. Throws by default, and
984      * cannot be overridden.
985      */
986     @NonNull
987     @Override
getType(@onNull Uri uri)988     public final String getType(@NonNull Uri uri) {
989         throw new UnsupportedOperationException("getType not supported");
990     }
991 
992     /**
993      * Implementation is provided by the parent class. Throws by default, and
994      * cannot be overridden.
995      */
996     @NonNull
997     @Override
canonicalize(@onNull Uri uri)998     public final Uri canonicalize(@NonNull Uri uri) {
999         throw new UnsupportedOperationException("Canonicalize not supported");
1000     }
1001 
1002     /**
1003      * Implementation is provided by the parent class. Throws by default, and
1004      * cannot be overridden.
1005      */
1006     @NonNull
1007     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)1008     public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
1009             @Nullable String selection, @Nullable String[] selectionArgs,
1010             @Nullable String sortOrder) {
1011         // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary
1012         // transport method. We override that, and don't ever delegate to this method.
1013         throw new UnsupportedOperationException("Pre-Android-O query format not supported.");
1014     }
1015 
1016     /**
1017      * Implementation is provided by the parent class. Throws by default, and
1018      * cannot be overridden.
1019      */
1020     @NonNull
1021     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal)1022     public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
1023             @Nullable String selection, @Nullable String[] selectionArgs,
1024             @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
1025         // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary
1026         // transport method. We override that, and don't ever delegate to this metohd.
1027         throw new UnsupportedOperationException("Pre-Android-O query format not supported.");
1028     }
1029 
1030     /**
1031      * Implementation is provided by the parent class. Throws by default, and
1032      * cannot be overridden.
1033      */
1034     @NonNull
1035     @Override
insert(@onNull Uri uri, @NonNull ContentValues values)1036     public final Uri insert(@NonNull Uri uri, @NonNull ContentValues values) {
1037         throw new UnsupportedOperationException("Insert not supported");
1038     }
1039 
1040     /**
1041      * Implementation is provided by the parent class. Throws by default, and
1042      * cannot be overridden.
1043      */
1044     @Override
delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)1045     public final int delete(@NonNull Uri uri, @Nullable String selection,
1046             @Nullable String[] selectionArgs) {
1047         throw new UnsupportedOperationException("Delete not supported");
1048     }
1049 
1050     /**
1051      * Implementation is provided by the parent class. Throws by default, and
1052      * cannot be overridden.
1053      */
1054     @Override
update(@onNull Uri uri, @NonNull ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)1055     public final int update(@NonNull Uri uri, @NonNull ContentValues values,
1056             @Nullable String selection, @Nullable String[] selectionArgs) {
1057         throw new UnsupportedOperationException("Update not supported");
1058     }
1059 
1060     /**
1061      * Manages rendering the preview of media items on given instances of {@link Surface}.
1062      *
1063      * <p>The methods of this class are meant to be asynchronous, and should not block by performing
1064      * any heavy operation.
1065      * <p>Note that a single CloudMediaSurfaceController instance would be responsible for
1066      * rendering multiple media items associated with multiple surfaces.
1067      */
1068     @SuppressLint("PackageLayering") // We need to pass in a Surface which can be prepared for
1069     // rendering a media item.
1070     public static abstract class CloudMediaSurfaceController {
1071 
1072         /**
1073          * Creates any player resource(s) needed for rendering.
1074          */
onPlayerCreate()1075         public abstract void onPlayerCreate();
1076 
1077         /**
1078          * Releases any player resource(s) used for rendering.
1079          */
onPlayerRelease()1080         public abstract void onPlayerRelease();
1081 
1082         /**
1083          * Indicates creation of the given {@link Surface} with given {@code surfaceId} for
1084          * rendering the preview of a media item with given {@code mediaId}.
1085          *
1086          * <p>This is called immediately after the surface is first created. Implementations of this
1087          * should start up whatever rendering code they desire.
1088          * <p>Note that the given media item remains associated with the given surface id till the
1089          * {@link Surface} is destroyed.
1090          *
1091          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
1092          * @param surface instance of the {@link Surface} on which the media item should be rendered
1093          * @param mediaId id which uniquely identifies the media to be rendered
1094          *
1095          * @see SurfaceHolder.Callback#surfaceCreated(SurfaceHolder)
1096          */
onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)1097         public abstract void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
1098                 @NonNull String mediaId);
1099 
1100         /**
1101          * Indicates structural changes (format or size) in the {@link Surface} for rendering.
1102          *
1103          * <p>This method is always called at least once, after {@link #onSurfaceCreated}.
1104          *
1105          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
1106          * @param format the new {@link PixelFormat} of the surface
1107          * @param width the new width of the {@link Surface}
1108          * @param height the new height of the {@link Surface}
1109          *
1110          * @see SurfaceHolder.Callback#surfaceChanged(SurfaceHolder, int, int, int)
1111          */
onSurfaceChanged(int surfaceId, int format, int width, int height)1112         public abstract void onSurfaceChanged(int surfaceId, int format, int width, int height);
1113 
1114         /**
1115          * Indicates destruction of a {@link Surface} with given {@code surfaceId}.
1116          *
1117          * <p>This is called immediately before a surface is being destroyed. After returning from
1118          * this call, you should no longer try to access this surface.
1119          *
1120          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
1121          *
1122          * @see SurfaceHolder.Callback#surfaceDestroyed(SurfaceHolder)
1123          */
onSurfaceDestroyed(int surfaceId)1124         public abstract void onSurfaceDestroyed(int surfaceId);
1125 
1126         /**
1127          * Start playing the preview of the media associated with the given surface id. If
1128          * playback had previously been paused, playback will continue from where it was paused.
1129          * If playback had been stopped, or never started before, playback will start at the
1130          * beginning.
1131          *
1132          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
1133          */
onMediaPlay(int surfaceId)1134         public abstract void onMediaPlay(int surfaceId);
1135 
1136         /**
1137          * Pauses the playback of the media associated with the given surface id.
1138          *
1139          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
1140          */
onMediaPause(int surfaceId)1141         public abstract void onMediaPause(int surfaceId);
1142 
1143         /**
1144          * Seeks the media associated with the given surface id to specified timestamp.
1145          *
1146          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
1147          * @param timestampMillis the timestamp in milliseconds from the start to seek to
1148          */
onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)1149         public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis);
1150 
1151         /**
1152          * Changes the configuration parameters for the CloudMediaSurfaceController.
1153          *
1154          * @param config the updated config to change to. This can include config changes for the
1155          * following:
1156          * <ul>
1157          * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
1158          * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
1159          * </ul>
1160          */
onConfigChange(@onNull Bundle config)1161         public abstract void onConfigChange(@NonNull Bundle config);
1162 
1163         /**
1164          * Indicates destruction of this CloudMediaSurfaceController object.
1165          *
1166          * <p>This CloudMediaSurfaceController object should no longer be in use after this method
1167          * has been called.
1168          *
1169          * <p>Note that it is possible for this method to be called directly without
1170          * {@link #onPlayerRelease} being called, hence you should release any resources associated
1171          * with this CloudMediaSurfaceController object, or perform any cleanup required in this
1172          * method.
1173          */
onDestroy()1174         public abstract void onDestroy();
1175     }
1176 
1177     /**
1178      * This class is used by {@link CloudMediaProvider} to send {@link Surface} state updates to
1179      * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
1180      *
1181      * @see MediaStore#ACTION_PICK_IMAGES
1182      */
1183     public static final class CloudMediaSurfaceStateChangedCallback {
1184 
1185         /** {@hide} */
1186         @IntDef(flag = true, prefix = { "PLAYBACK_STATE_" }, value = {
1187                 PLAYBACK_STATE_BUFFERING,
1188                 PLAYBACK_STATE_READY,
1189                 PLAYBACK_STATE_STARTED,
1190                 PLAYBACK_STATE_PAUSED,
1191                 PLAYBACK_STATE_COMPLETED,
1192                 PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE,
1193                 PLAYBACK_STATE_ERROR_PERMANENT_FAILURE,
1194                 PLAYBACK_STATE_MEDIA_SIZE_CHANGED
1195         })
1196         @Retention(RetentionPolicy.SOURCE)
1197         public @interface PlaybackState {}
1198 
1199         /**
1200          * Constant to notify that the playback is buffering
1201          */
1202         public static final int PLAYBACK_STATE_BUFFERING = 1;
1203 
1204         /**
1205          * Constant to notify that the playback is ready to be played
1206          */
1207         public static final int PLAYBACK_STATE_READY = 2;
1208 
1209         /**
1210          * Constant to notify that the playback has started
1211          */
1212         public static final int PLAYBACK_STATE_STARTED = 3;
1213 
1214         /**
1215          * Constant to notify that the playback is paused.
1216          */
1217         public static final int PLAYBACK_STATE_PAUSED = 4;
1218 
1219         /**
1220          * Constant to notify that the playback has completed
1221          */
1222         public static final int PLAYBACK_STATE_COMPLETED = 5;
1223 
1224         /**
1225          * Constant to notify that the playback has failed with a retriable error.
1226          */
1227         public static final int PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE = 6;
1228 
1229         /**
1230          * Constant to notify that the playback has failed with a permanent error.
1231          */
1232         public static final int PLAYBACK_STATE_ERROR_PERMANENT_FAILURE = 7;
1233 
1234         /**
1235          * Constant to notify that the media size is first known or has changed.
1236          *
1237          * Pass the width and height of the media as a {@link Point} inside the {@link Bundle} with
1238          * {@link ContentResolver#EXTRA_SIZE} as the key.
1239          *
1240          * @see CloudMediaSurfaceStateChangedCallback#setPlaybackState(int, int, Bundle)
1241          * @see MediaPlayer.OnVideoSizeChangedListener#onVideoSizeChanged(MediaPlayer, int, int)
1242          */
1243         public static final int PLAYBACK_STATE_MEDIA_SIZE_CHANGED = 8;
1244 
1245         private final ICloudMediaSurfaceStateChangedCallback mCallback;
1246 
CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback)1247         CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback) {
1248             mCallback = callback;
1249         }
1250 
1251         /**
1252          * This is called to notify playback state update for a {@link Surface}
1253          * on the picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
1254          *
1255          * @param surfaceId id which uniquely identifies a {@link Surface}
1256          * @param playbackState playback state to notify picker about
1257          * @param playbackStateInfo {@link Bundle} which may contain extra information about the
1258          *                          playback state, such as media size, progress/seek info or
1259          *                          details about errors.
1260          */
setPlaybackState(int surfaceId, @PlaybackState int playbackState, @Nullable Bundle playbackStateInfo)1261         public void setPlaybackState(int surfaceId, @PlaybackState int playbackState,
1262                 @Nullable Bundle playbackStateInfo) {
1263             try {
1264                 mCallback.setPlaybackState(surfaceId, playbackState, playbackStateInfo);
1265             } catch (Exception e) {
1266                 Log.w(TAG, "Failed to notify playback state (" + playbackState + ") for "
1267                         + "surfaceId: " + surfaceId + " ; playbackStateInfo: " + playbackStateInfo,
1268                         e);
1269             }
1270         }
1271 
1272         /**
1273          * Returns the underliying {@link IBinder} object.
1274          *
1275          * @hide
1276          */
getIBinder()1277         public IBinder getIBinder() {
1278             return mCallback.asBinder();
1279         }
1280     }
1281 
1282     /**
1283      * {@link Binder} object backing a {@link CloudMediaSurfaceController} instance.
1284      *
1285      * @hide
1286      */
1287     public static class CloudMediaSurfaceControllerWrapper
1288             extends ICloudMediaSurfaceController.Stub {
1289 
1290         final private CloudMediaSurfaceController mSurfaceController;
1291 
CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController)1292         CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController) {
1293             mSurfaceController = surfaceController;
1294         }
1295 
1296         @Override
onPlayerCreate()1297         public void onPlayerCreate() {
1298             Log.i(TAG, "Creating player.");
1299             mSurfaceController.onPlayerCreate();
1300         }
1301 
1302         @Override
onPlayerRelease()1303         public void onPlayerRelease() {
1304             Log.i(TAG, "Releasing player.");
1305             mSurfaceController.onPlayerRelease();
1306         }
1307 
1308         @Override
onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)1309         public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
1310                 @NonNull String mediaId) {
1311             Log.i(TAG, "Surface prepared. SurfaceId: " + surfaceId + ". MediaId: " + mediaId);
1312             mSurfaceController.onSurfaceCreated(surfaceId, surface, mediaId);
1313         }
1314 
1315         @Override
onSurfaceChanged(int surfaceId, int format, int width, int height)1316         public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
1317             Log.i(TAG, "Surface changed. SurfaceId: " + surfaceId + ". Format: " + format
1318                     + ". Width: " + width + ". Height: " + height);
1319             mSurfaceController.onSurfaceChanged(surfaceId, format, width, height);
1320         }
1321 
1322         @Override
onSurfaceDestroyed(int surfaceId)1323         public void onSurfaceDestroyed(int surfaceId) {
1324             Log.i(TAG, "Surface released. SurfaceId: " + surfaceId);
1325             mSurfaceController.onSurfaceDestroyed(surfaceId);
1326         }
1327 
1328         @Override
onMediaPlay(int surfaceId)1329         public void onMediaPlay(int surfaceId) {
1330             Log.i(TAG, "Media played. SurfaceId: " + surfaceId);
1331             mSurfaceController.onMediaPlay(surfaceId);
1332         }
1333 
1334         @Override
onMediaPause(int surfaceId)1335         public void onMediaPause(int surfaceId) {
1336             Log.i(TAG, "Media paused. SurfaceId: " + surfaceId);
1337             mSurfaceController.onMediaPause(surfaceId);
1338         }
1339 
1340         @Override
onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)1341         public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
1342             Log.i(TAG, "Media seeked. SurfaceId: " + surfaceId + ". Seek timestamp(ms): "
1343                     + timestampMillis);
1344             mSurfaceController.onMediaSeekTo(surfaceId, timestampMillis);
1345         }
1346 
1347         @Override
onConfigChange(@onNull Bundle config)1348         public void onConfigChange(@NonNull Bundle config) {
1349             Log.i(TAG, "Config changed. Updated config params: " + config);
1350             mSurfaceController.onConfigChange(config);
1351         }
1352 
1353         @Override
onDestroy()1354         public void onDestroy() {
1355             Log.i(TAG, "Controller destroyed");
1356             mSurfaceController.onDestroy();
1357         }
1358     }
1359 
1360     /**
1361      * @hide
1362      */
1363     private class AsyncContentProviderWrapper extends IAsyncContentProvider.Stub {
1364 
1365         @Override
openMedia(String mediaId, RemoteCallback remoteCallback)1366         public void openMedia(String mediaId, RemoteCallback remoteCallback) {
1367             try {
1368                 ParcelFileDescriptor pfd = onOpenMedia(mediaId,/* extras */
1369                         null,/* cancellationSignal */ null);
1370                 sendResult(pfd, null, remoteCallback);
1371             } catch (Exception e) {
1372                 sendResult(null, e, remoteCallback);
1373             }
1374         }
1375 
sendResult(ParcelFileDescriptor pfd, Throwable throwable, RemoteCallback remoteCallback)1376         private void sendResult(ParcelFileDescriptor pfd, Throwable throwable,
1377                 RemoteCallback remoteCallback) {
1378             Bundle bundle = new Bundle();
1379             if (pfd == null && throwable == null) {
1380                 throw new IllegalStateException("Expected ParcelFileDescriptor or an exception.");
1381             }
1382             if (pfd != null) {
1383                 bundle.putParcelable(EXTRA_FILE_DESCRIPTOR, pfd);
1384             }
1385             if (throwable != null) {
1386                 bundle.putString(EXTRA_ERROR_MESSAGE, throwable.getMessage());
1387             }
1388             remoteCallback.sendResult(bundle);
1389         }
1390     }
1391 }
1392