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_SURFACE_CONTROLLER;
26 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
27 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK;
28 import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
29 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER;
30 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
31 import static android.provider.CloudMediaProviderContract.URI_PATH_ALBUM;
32 import static android.provider.CloudMediaProviderContract.URI_PATH_DELETED_MEDIA;
33 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA;
34 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO;
35 import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER;
36 
37 import android.annotation.DurationMillisLong;
38 import android.annotation.IntDef;
39 import android.annotation.NonNull;
40 import android.annotation.Nullable;
41 import android.annotation.SuppressLint;
42 import android.content.ContentProvider;
43 import android.content.ContentResolver;
44 import android.content.ContentValues;
45 import android.content.Context;
46 import android.content.UriMatcher;
47 import android.content.pm.ProviderInfo;
48 import android.content.res.AssetFileDescriptor;
49 import android.database.Cursor;
50 import android.graphics.PixelFormat;
51 import android.graphics.Point;
52 import android.media.MediaPlayer;
53 import android.net.Uri;
54 import android.os.Binder;
55 import android.os.Bundle;
56 import android.os.CancellationSignal;
57 import android.os.IBinder;
58 import android.os.ParcelFileDescriptor;
59 import android.os.RemoteCallback;
60 import android.util.DisplayMetrics;
61 import android.util.Log;
62 import android.view.Surface;
63 import android.view.SurfaceHolder;
64 
65 import java.io.FileNotFoundException;
66 import java.lang.annotation.Retention;
67 import java.lang.annotation.RetentionPolicy;
68 import java.util.Objects;
69 
70 /**
71  * Base class for a cloud media provider. A cloud media provider offers read-only access to durable
72  * media files, specifically photos and videos stored on a local disk, or files in a cloud storage
73  * service. To create a cloud media provider, extend this class, implement the abstract methods,
74  * and add it to your manifest like this:
75  *
76  * <pre class="prettyprint">&lt;manifest&gt;
77  *    ...
78  *    &lt;application&gt;
79  *        ...
80  *        &lt;provider
81  *            android:name="com.example.MyCloudProvider"
82  *            android:authorities="com.example.mycloudprovider"
83  *            android:exported="true"
84  *            android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
85  *            &lt;intent-filter&gt;
86  *                &lt;action android:name="android.content.action.CLOUD_MEDIA_PROVIDER" /&gt;
87  *            &lt;/intent-filter&gt;
88  *        &lt;/provider&gt;
89  *        ...
90  *    &lt;/application&gt;
91  *&lt;/manifest&gt;</pre>
92  * <p>
93  * When defining your provider, you must protect it with the
94  * {@link CloudMediaProviderContract#MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION}, which is a permission
95  * only the system can obtain, trying to define an unprotected {@link CloudMediaProvider} will
96  * result in a {@link SecurityException}.
97  * <p>
98  * Applications cannot use a cloud media provider directly; they must go through
99  * {@link MediaStore#ACTION_PICK_IMAGES} which requires a user to actively navigate and select
100  * media items. When a user selects a media item through that UI, the system issues narrow URI
101  * permission grants to the requesting application.
102  * <h3>Media items</h3>
103  * <p>
104  * A media item must be an openable stream (with a specific MIME type). Media items can belong to
105  * zero or more albums. Albums cannot contain other albums.
106  * <p>
107  * Each item under a provider is uniquely referenced by its media or album id, which must not
108  * change which must be unique across all collection IDs as returned by
109  * {@link #onGetMediaCollectionInfo}.
110  *
111  * @see MediaStore#ACTION_PICK_IMAGES
112  */
113 public abstract class CloudMediaProvider extends ContentProvider {
114     private static final String TAG = "CloudMediaProvider";
115 
116     private static final int MATCH_MEDIAS = 1;
117     private static final int MATCH_DELETED_MEDIAS = 2;
118     private static final int MATCH_ALBUMS = 3;
119     private static final int MATCH_MEDIA_COLLECTION_INFO = 4;
120     private static final int MATCH_SURFACE_CONTROLLER = 5;
121 
122     private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true;
123     private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false;
124 
125     private final UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
126     private volatile int mMediaStoreAuthorityAppId;
127 
128     private String mAuthority;
129 
130 
131     /**
132      * Implementation is provided by the parent class. Cannot be overridden.
133      */
134     @Override
attachInfo(@onNull Context context, @NonNull ProviderInfo info)135     public final void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
136         registerAuthority(info.authority);
137 
138         super.attachInfo(context, info);
139     }
140 
registerAuthority(String authority)141     private void registerAuthority(String authority) {
142         mAuthority = authority;
143         mMatcher.addURI(authority, URI_PATH_MEDIA, MATCH_MEDIAS);
144         mMatcher.addURI(authority, URI_PATH_DELETED_MEDIA, MATCH_DELETED_MEDIAS);
145         mMatcher.addURI(authority, URI_PATH_ALBUM, MATCH_ALBUMS);
146         mMatcher.addURI(authority, URI_PATH_MEDIA_COLLECTION_INFO, MATCH_MEDIA_COLLECTION_INFO);
147         mMatcher.addURI(authority, URI_PATH_SURFACE_CONTROLLER, MATCH_SURFACE_CONTROLLER);
148     }
149 
150     /**
151      * Returns {@link Bundle} containing binder to {@link IAsyncContentProvider}.
152      *
153      * @hide
154      */
155     @NonNull
onGetAsyncContentProvider()156     public final Bundle onGetAsyncContentProvider() {
157         Bundle bundle = new Bundle();
158         bundle.putBinder(EXTRA_ASYNC_CONTENT_PROVIDER,
159                 (new AsyncContentProviderWrapper()).asBinder());
160         return bundle;
161     }
162 
163     /**
164      * Returns metadata about the media collection itself.
165      * <p>
166      * This is useful for the OS to determine if its cache of media items in the collection is
167      * still valid and if a full or incremental sync is required with {@link #onQueryMedia}.
168      * <p>
169      * This method might be called by the OS frequently and is performance critical, hence it should
170      * avoid long running operations.
171      * <p>
172      * If the provider handled any filters in {@code extras}, it must add the key to the
173      * {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned {@link Bundle}.
174      *
175      * @param extras containing keys to filter result:
176      * <ul>
177      * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
178      * </ul>
179      *
180      * @return {@link Bundle} containing {@link CloudMediaProviderContract.MediaCollectionInfo}
181      * <ul>
182      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}
183      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}
184      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}
185      * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}
186      * </ul>
187      */
188     @SuppressWarnings("unused")
189     @NonNull
onGetMediaCollectionInfo(@onNull Bundle extras)190     public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
191 
192     /**
193      * Returns a cursor representing all media items in the media collection optionally filtered by
194      * {@code extras} and sorted in reverse chronological order of
195      * {@link CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS}, i.e. most recent items
196      * first.
197      * <p>
198      * The cloud media provider must set the
199      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
200      * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
201      * returned {@link Cursor}.
202      * <p>
203      * If the cloud media provider handled any filters in {@code extras}, it must add the key to
204      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
205      * {@link Cursor#setExtras} {@link Bundle}.
206      *
207      * @param extras containing keys to filter media items:
208      * <ul>
209      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
210      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
211      * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
212      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
213      * </ul>
214      * @return cursor representing media items containing all
215      * {@link CloudMediaProviderContract.MediaColumns} columns
216      */
217     @SuppressWarnings("unused")
218     @NonNull
onQueryMedia(@onNull Bundle extras)219     public abstract Cursor onQueryMedia(@NonNull Bundle extras);
220 
221     /**
222      * Returns a {@link Cursor} representing all deleted media items in the entire media collection
223      * within the current provider version as returned by {@link #onGetMediaCollectionInfo}. These
224      * items can be optionally filtered by {@code extras}.
225      * <p>
226      * The cloud media provider must set the
227      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
228      * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
229      * returned {@link Cursor}.
230      * <p>
231      * If the provider handled any filters in {@code extras}, it must add the key to
232      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
233      * {@link Cursor#setExtras} {@link Bundle}.
234      *
235      * @param extras containing keys to filter deleted media items:
236      * <ul>
237      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
238      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
239      * </ul>
240      * @return cursor representing deleted media items containing just the
241      * {@link CloudMediaProviderContract.MediaColumns#ID} column
242      */
243     @SuppressWarnings("unused")
244     @NonNull
onQueryDeletedMedia(@onNull Bundle extras)245     public abstract Cursor onQueryDeletedMedia(@NonNull Bundle extras);
246 
247     /**
248      * Returns a cursor representing all album items in the media collection optionally filtered
249      * by {@code extras} and sorted in reverse chronological order of
250      * {@link CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}, i.e. most recent items
251      * first.
252      * <p>
253      * The cloud media provider must set the
254      * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
255      * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
256      * returned {@link Cursor}.
257      * <p>
258      * If the provider handled any filters in {@code extras}, it must add the key to
259      * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
260      * {@link Cursor#setExtras} {@link Bundle}.
261      *
262      * @param extras containing keys to filter album items:
263      * <ul>
264      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
265      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
266      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
267      * </ul>
268      * @return cursor representing album items containing all
269      * {@link CloudMediaProviderContract.AlbumColumns} columns
270      */
271     @SuppressWarnings("unused")
272     @NonNull
onQueryAlbums(@onNull Bundle extras)273     public Cursor onQueryAlbums(@NonNull Bundle extras) {
274         throw new UnsupportedOperationException("queryAlbums not supported");
275     }
276 
277     /**
278      * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId}
279      * <p>The cloud media provider should strictly return thumbnail in the original
280      * {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item.
281      * <p>
282      * This is expected to be a much lower resolution version than the item returned by
283      * {@link #onOpenMedia}.
284      * <p>
285      * If you block while downloading content, you should periodically check
286      * {@link CancellationSignal#isCanceled()} to abort abandoned open requests.
287      *
288      * @param mediaId the media item to return
289      * @param size the dimensions of the thumbnail to return. The returned file descriptor doesn't
290      * have to match the {@code size} precisely because the OS will adjust the dimensions before
291      * usage. Implementations can return close approximations especially if the approximation is
292      * already locally on the device and doesn't require downloading from the cloud.
293      * @param extras to modify the way the fd is opened, e.g. for video files we may request a
294      * thumbnail image instead of a video with
295      * {@link CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL}
296      * @param signal used by the OS to signal if the request should be cancelled
297      * @return read-only file descriptor for accessing the thumbnail for the media file
298      *
299      * @see #onOpenMedia
300      * @see CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL
301      */
302     @SuppressWarnings("unused")
303     @NonNull
onOpenPreview(@onNull String mediaId, @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal)304     public abstract AssetFileDescriptor onOpenPreview(@NonNull String mediaId,
305             @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal)
306             throws FileNotFoundException;
307 
308     /**
309      * Returns the full size media item identified by {@code mediaId}.
310      * <p>
311      * If you block while downloading content, you should periodically check
312      * {@link CancellationSignal#isCanceled()} to abort abandoned open requests.
313      *
314      * @param mediaId the media item to return
315      * @param extras to modify the way the fd is opened, there's none at the moment, but some
316      * might be implemented in the future
317      * @param signal used by the OS to signal if the request should be cancelled
318      * @return read-only file descriptor for accessing the media file
319      *
320      * @see #onOpenPreview
321      */
322     @SuppressWarnings("unused")
323     @NonNull
onOpenMedia(@onNull String mediaId, @Nullable Bundle extras, @Nullable CancellationSignal signal)324     public abstract ParcelFileDescriptor onOpenMedia(@NonNull String mediaId,
325             @Nullable Bundle extras, @Nullable CancellationSignal signal)
326             throws FileNotFoundException;
327 
328     /**
329      * Returns a {@link CloudMediaSurfaceController} used for rendering the preview of media items,
330      * or null if preview rendering is not supported.
331      *
332      * @param config containing configuration parameters for {@link CloudMediaSurfaceController}
333      * <ul>
334      * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
335      * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
336      * </ul>
337      * @param callback {@link CloudMediaSurfaceStateChangedCallback} to send state updates for
338      *                 {@link Surface} to picker launched via {@link MediaStore#ACTION_PICK_IMAGES}
339      */
340     @Nullable
onCreateCloudMediaSurfaceController(@onNull Bundle config, @NonNull CloudMediaSurfaceStateChangedCallback callback)341     public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config,
342             @NonNull CloudMediaSurfaceStateChangedCallback callback) {
343         return null;
344     }
345 
346     /**
347      * Implementation is provided by the parent class. Cannot be overridden.
348      */
349     @Override
350     @NonNull
call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)351     public final Bundle call(@NonNull String method, @Nullable String arg,
352             @Nullable Bundle extras) {
353         if (!method.startsWith("android:")) {
354             // Ignore non-platform methods
355             return super.call(method, arg, extras);
356         }
357 
358         try {
359             return callUnchecked(method, arg, extras);
360         } catch (FileNotFoundException e) {
361             throw new RuntimeException(e);
362         }
363     }
364 
callUnchecked(@onNull String method, @Nullable String arg, @Nullable Bundle extras)365     private Bundle callUnchecked(@NonNull String method, @Nullable String arg,
366                                  @Nullable Bundle extras)
367             throws FileNotFoundException {
368         if (extras == null) {
369             extras = new Bundle();
370         }
371         Bundle result = new Bundle();
372         if (METHOD_GET_MEDIA_COLLECTION_INFO.equals(method)) {
373             long startTime = System.currentTimeMillis();
374             result = onGetMediaCollectionInfo(extras);
375             CmpApiVerifier.verifyApiResult(new CmpApiResult(
376                             CmpApiVerifier.CloudMediaProviderApis.OnGetMediaCollectionInfo, result),
377                     System.currentTimeMillis() - startTime, mAuthority);
378         } else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) {
379             result = onCreateCloudMediaSurfaceController(extras);
380         } else if (METHOD_GET_ASYNC_CONTENT_PROVIDER.equals(method)) {
381             result = onGetAsyncContentProvider();
382         } else {
383             throw new UnsupportedOperationException("Method not supported " + method);
384         }
385         return result;
386     }
387 
onCreateCloudMediaSurfaceController(@onNull Bundle extras)388     private Bundle onCreateCloudMediaSurfaceController(@NonNull Bundle extras) {
389         Objects.requireNonNull(extras);
390 
391         final IBinder binder = extras.getBinder(EXTRA_SURFACE_STATE_CALLBACK);
392         if (binder == null) {
393             throw new IllegalArgumentException("Missing surface state callback");
394         }
395 
396         final boolean enableLoop = extras.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
397                 DEFAULT_LOOPING_PLAYBACK_ENABLED);
398         final boolean muteAudio = extras.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
399                 DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
400         final String authority = extras.getString(EXTRA_AUTHORITY);
401         final CloudMediaSurfaceStateChangedCallback callback =
402                 new CloudMediaSurfaceStateChangedCallback(
403                         ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder));
404         final Bundle config = new Bundle();
405         config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, enableLoop);
406         config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, muteAudio);
407         config.putString(EXTRA_AUTHORITY, authority);
408         final CloudMediaSurfaceController controller =
409                 onCreateCloudMediaSurfaceController(config, callback);
410         if (controller == null) {
411             Log.d(TAG, "onCreateCloudMediaSurfaceController returned null");
412             return Bundle.EMPTY;
413         }
414 
415         Bundle result = new Bundle();
416         result.putBinder(EXTRA_SURFACE_CONTROLLER,
417                 new CloudMediaSurfaceControllerWrapper(controller).asBinder());
418         return result;
419     }
420 
421     /**
422      * Implementation is provided by the parent class. Cannot be overridden.
423      *
424      * @see #onOpenMedia
425      */
426     @NonNull
427     @Override
openFile(@onNull Uri uri, @NonNull String mode)428     public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
429             throws FileNotFoundException {
430         return openFile(uri, mode, null);
431     }
432 
433     /**
434      * Implementation is provided by the parent class. Cannot be overridden.
435      *
436      * @see #onOpenMedia
437      */
438     @NonNull
439     @Override
openFile(@onNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal)440     public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode,
441             @Nullable CancellationSignal signal) throws FileNotFoundException {
442         String mediaId = uri.getLastPathSegment();
443 
444         long startTime = System.currentTimeMillis();
445         ParcelFileDescriptor result = onOpenMedia(mediaId, /* extras */ null, signal);
446         CmpApiVerifier.verifyApiResult(new CmpApiResult(
447                         CmpApiVerifier.CloudMediaProviderApis.OnOpenMedia, result),
448                 System.currentTimeMillis() - startTime, mAuthority);
449         return result;
450     }
451 
452     /**
453      * Implementation is provided by the parent class. Cannot be overridden.
454      *
455      * @see #onOpenPreview
456      * @see #onOpenMedia
457      */
458     @NonNull
459     @Override
openTypedAssetFile(@onNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts)460     public final AssetFileDescriptor openTypedAssetFile(@NonNull Uri uri,
461             @NonNull String mimeTypeFilter, @Nullable Bundle opts) throws FileNotFoundException {
462         return openTypedAssetFile(uri, mimeTypeFilter, opts, null);
463     }
464 
465     /**
466      * Implementation is provided by the parent class. Cannot be overridden.
467      *
468      * @see #onOpenPreview
469      * @see #onOpenMedia
470      */
471     @NonNull
472     @Override
openTypedAssetFile( @onNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts, @Nullable CancellationSignal signal)473     public final AssetFileDescriptor openTypedAssetFile(
474             @NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts,
475             @Nullable CancellationSignal signal) throws FileNotFoundException {
476         final String mediaId = uri.getLastPathSegment();
477         final Bundle bundle = new Bundle();
478         Point previewSize = null;
479 
480         final DisplayMetrics screenMetrics = getContext().getResources().getDisplayMetrics();
481         int minPreviewLength = Math.min(screenMetrics.widthPixels, screenMetrics.heightPixels);
482 
483         if (opts != null) {
484             bundle.putBoolean(EXTRA_MEDIASTORE_THUMB, opts.getBoolean(EXTRA_MEDIASTORE_THUMB));
485 
486             if (opts.containsKey(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)) {
487                 bundle.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true);
488                 minPreviewLength = minPreviewLength / 2;
489             }
490 
491             previewSize = opts.getParcelable(ContentResolver.EXTRA_SIZE);
492         }
493 
494         if (previewSize == null) {
495             previewSize = new Point(minPreviewLength, minPreviewLength);
496         }
497 
498         long startTime = System.currentTimeMillis();
499         AssetFileDescriptor result = onOpenPreview(mediaId, previewSize, bundle, signal);
500         CmpApiVerifier.verifyApiResult(new CmpApiResult(
501                         CmpApiVerifier.CloudMediaProviderApis.OnOpenPreview, result, previewSize),
502                 System.currentTimeMillis() - startTime, mAuthority);
503         return result;
504     }
505 
506     /**
507      * Implementation is provided by the parent class. Cannot be overridden.
508      *
509      * @see #onQueryMedia
510      * @see #onQueryDeletedMedia
511      * @see #onQueryAlbums
512      */
513     @NonNull
514     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)515     public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
516             @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
517         if (queryArgs == null) {
518             queryArgs = new Bundle();
519         }
520         Cursor result;
521         long startTime = System.currentTimeMillis();
522         switch (mMatcher.match(uri)) {
523             case MATCH_MEDIAS:
524                 result = onQueryMedia(queryArgs);
525                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
526                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryMedia, result),
527                         System.currentTimeMillis() - startTime, mAuthority);
528                 break;
529             case MATCH_DELETED_MEDIAS:
530                 result = onQueryDeletedMedia(queryArgs);
531                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
532                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryDeletedMedia, result),
533                         System.currentTimeMillis() - startTime, mAuthority);
534                 break;
535             case MATCH_ALBUMS:
536                 result = onQueryAlbums(queryArgs);
537                 CmpApiVerifier.verifyApiResult(new CmpApiResult(
538                                 CmpApiVerifier.CloudMediaProviderApis.OnQueryAlbums, result),
539                         System.currentTimeMillis() - startTime, mAuthority);
540                 break;
541             default:
542                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
543         }
544         return result;
545     }
546 
547     /**
548      * Implementation is provided by the parent class. Throws by default, and
549      * cannot be overridden.
550      */
551     @NonNull
552     @Override
getType(@onNull Uri uri)553     public final String getType(@NonNull Uri uri) {
554         throw new UnsupportedOperationException("getType not supported");
555     }
556 
557     /**
558      * Implementation is provided by the parent class. Throws by default, and
559      * cannot be overridden.
560      */
561     @NonNull
562     @Override
canonicalize(@onNull Uri uri)563     public final Uri canonicalize(@NonNull Uri uri) {
564         throw new UnsupportedOperationException("Canonicalize not supported");
565     }
566 
567     /**
568      * Implementation is provided by the parent class. Throws by default, and
569      * cannot be overridden.
570      */
571     @NonNull
572     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)573     public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
574             @Nullable String selection, @Nullable String[] selectionArgs,
575             @Nullable String sortOrder) {
576         // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary
577         // transport method. We override that, and don't ever delegate to this method.
578         throw new UnsupportedOperationException("Pre-Android-O query format not supported.");
579     }
580 
581     /**
582      * Implementation is provided by the parent class. Throws by default, and
583      * cannot be overridden.
584      */
585     @NonNull
586     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal)587     public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
588             @Nullable String selection, @Nullable String[] selectionArgs,
589             @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
590         // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary
591         // transport method. We override that, and don't ever delegate to this metohd.
592         throw new UnsupportedOperationException("Pre-Android-O query format not supported.");
593     }
594 
595     /**
596      * Implementation is provided by the parent class. Throws by default, and
597      * cannot be overridden.
598      */
599     @NonNull
600     @Override
insert(@onNull Uri uri, @NonNull ContentValues values)601     public final Uri insert(@NonNull Uri uri, @NonNull ContentValues values) {
602         throw new UnsupportedOperationException("Insert not supported");
603     }
604 
605     /**
606      * Implementation is provided by the parent class. Throws by default, and
607      * cannot be overridden.
608      */
609     @Override
delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)610     public final int delete(@NonNull Uri uri, @Nullable String selection,
611             @Nullable String[] selectionArgs) {
612         throw new UnsupportedOperationException("Delete not supported");
613     }
614 
615     /**
616      * Implementation is provided by the parent class. Throws by default, and
617      * cannot be overridden.
618      */
619     @Override
update(@onNull Uri uri, @NonNull ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)620     public final int update(@NonNull Uri uri, @NonNull ContentValues values,
621             @Nullable String selection, @Nullable String[] selectionArgs) {
622         throw new UnsupportedOperationException("Update not supported");
623     }
624 
625     /**
626      * Manages rendering the preview of media items on given instances of {@link Surface}.
627      *
628      * <p>The methods of this class are meant to be asynchronous, and should not block by performing
629      * any heavy operation.
630      * <p>Note that a single CloudMediaSurfaceController instance would be responsible for
631      * rendering multiple media items associated with multiple surfaces.
632      */
633     @SuppressLint("PackageLayering") // We need to pass in a Surface which can be prepared for
634     // rendering a media item.
635     public static abstract class CloudMediaSurfaceController {
636 
637         /**
638          * Creates any player resource(s) needed for rendering.
639          */
onPlayerCreate()640         public abstract void onPlayerCreate();
641 
642         /**
643          * Releases any player resource(s) used for rendering.
644          */
onPlayerRelease()645         public abstract void onPlayerRelease();
646 
647         /**
648          * Indicates creation of the given {@link Surface} with given {@code surfaceId} for
649          * rendering the preview of a media item with given {@code mediaId}.
650          *
651          * <p>This is called immediately after the surface is first created. Implementations of this
652          * should start up whatever rendering code they desire.
653          * <p>Note that the given media item remains associated with the given surface id till the
654          * {@link Surface} is destroyed.
655          *
656          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
657          * @param surface instance of the {@link Surface} on which the media item should be rendered
658          * @param mediaId id which uniquely identifies the media to be rendered
659          *
660          * @see SurfaceHolder.Callback#surfaceCreated(SurfaceHolder)
661          */
onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)662         public abstract void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
663                 @NonNull String mediaId);
664 
665         /**
666          * Indicates structural changes (format or size) in the {@link Surface} for rendering.
667          *
668          * <p>This method is always called at least once, after {@link #onSurfaceCreated}.
669          *
670          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
671          * @param format the new {@link PixelFormat} of the surface
672          * @param width the new width of the {@link Surface}
673          * @param height the new height of the {@link Surface}
674          *
675          * @see SurfaceHolder.Callback#surfaceChanged(SurfaceHolder, int, int, int)
676          */
onSurfaceChanged(int surfaceId, int format, int width, int height)677         public abstract void onSurfaceChanged(int surfaceId, int format, int width, int height);
678 
679         /**
680          * Indicates destruction of a {@link Surface} with given {@code surfaceId}.
681          *
682          * <p>This is called immediately before a surface is being destroyed. After returning from
683          * this call, you should no longer try to access this surface.
684          *
685          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
686          *
687          * @see SurfaceHolder.Callback#surfaceDestroyed(SurfaceHolder)
688          */
onSurfaceDestroyed(int surfaceId)689         public abstract void onSurfaceDestroyed(int surfaceId);
690 
691         /**
692          * Start playing the preview of the media associated with the given surface id. If
693          * playback had previously been paused, playback will continue from where it was paused.
694          * If playback had been stopped, or never started before, playback will start at the
695          * beginning.
696          *
697          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
698          */
onMediaPlay(int surfaceId)699         public abstract void onMediaPlay(int surfaceId);
700 
701         /**
702          * Pauses the playback of the media associated with the given surface id.
703          *
704          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
705          */
onMediaPause(int surfaceId)706         public abstract void onMediaPause(int surfaceId);
707 
708         /**
709          * Seeks the media associated with the given surface id to specified timestamp.
710          *
711          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
712          * @param timestampMillis the timestamp in milliseconds from the start to seek to
713          */
onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)714         public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis);
715 
716         /**
717          * Changes the configuration parameters for the CloudMediaSurfaceController.
718          *
719          * @param config the updated config to change to. This can include config changes for the
720          * following:
721          * <ul>
722          * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
723          * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
724          * </ul>
725          */
onConfigChange(@onNull Bundle config)726         public abstract void onConfigChange(@NonNull Bundle config);
727 
728         /**
729          * Indicates destruction of this CloudMediaSurfaceController object.
730          *
731          * <p>This CloudMediaSurfaceController object should no longer be in use after this method
732          * has been called.
733          *
734          * <p>Note that it is possible for this method to be called directly without
735          * {@link #onPlayerRelease} being called, hence you should release any resources associated
736          * with this CloudMediaSurfaceController object, or perform any cleanup required in this
737          * method.
738          */
onDestroy()739         public abstract void onDestroy();
740     }
741 
742     /**
743      * This class is used by {@link CloudMediaProvider} to send {@link Surface} state updates to
744      * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
745      *
746      * @see MediaStore#ACTION_PICK_IMAGES
747      */
748     public static final class CloudMediaSurfaceStateChangedCallback {
749 
750         /** {@hide} */
751         @IntDef(flag = true, prefix = { "PLAYBACK_STATE_" }, value = {
752                 PLAYBACK_STATE_BUFFERING,
753                 PLAYBACK_STATE_READY,
754                 PLAYBACK_STATE_STARTED,
755                 PLAYBACK_STATE_PAUSED,
756                 PLAYBACK_STATE_COMPLETED,
757                 PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE,
758                 PLAYBACK_STATE_ERROR_PERMANENT_FAILURE,
759                 PLAYBACK_STATE_MEDIA_SIZE_CHANGED
760         })
761         @Retention(RetentionPolicy.SOURCE)
762         public @interface PlaybackState {}
763 
764         /**
765          * Constant to notify that the playback is buffering
766          */
767         public static final int PLAYBACK_STATE_BUFFERING = 1;
768 
769         /**
770          * Constant to notify that the playback is ready to be played
771          */
772         public static final int PLAYBACK_STATE_READY = 2;
773 
774         /**
775          * Constant to notify that the playback has started
776          */
777         public static final int PLAYBACK_STATE_STARTED = 3;
778 
779         /**
780          * Constant to notify that the playback is paused.
781          */
782         public static final int PLAYBACK_STATE_PAUSED = 4;
783 
784         /**
785          * Constant to notify that the playback has completed
786          */
787         public static final int PLAYBACK_STATE_COMPLETED = 5;
788 
789         /**
790          * Constant to notify that the playback has failed with a retriable error.
791          */
792         public static final int PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE = 6;
793 
794         /**
795          * Constant to notify that the playback has failed with a permanent error.
796          */
797         public static final int PLAYBACK_STATE_ERROR_PERMANENT_FAILURE = 7;
798 
799         /**
800          * Constant to notify that the media size is first known or has changed.
801          *
802          * Pass the width and height of the media as a {@link Point} inside the {@link Bundle} with
803          * {@link ContentResolver#EXTRA_SIZE} as the key.
804          *
805          * @see CloudMediaSurfaceStateChangedCallback#setPlaybackState(int, int, Bundle)
806          * @see MediaPlayer.OnVideoSizeChangedListener#onVideoSizeChanged(MediaPlayer, int, int)
807          */
808         public static final int PLAYBACK_STATE_MEDIA_SIZE_CHANGED = 8;
809 
810         private final ICloudMediaSurfaceStateChangedCallback mCallback;
811 
CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback)812         CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback) {
813             mCallback = callback;
814         }
815 
816         /**
817          * This is called to notify playback state update for a {@link Surface}
818          * on the picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
819          *
820          * @param surfaceId id which uniquely identifies a {@link Surface}
821          * @param playbackState playback state to notify picker about
822          * @param playbackStateInfo {@link Bundle} which may contain extra information about the
823          *                          playback state, such as media size, progress/seek info or
824          *                          details about errors.
825          */
setPlaybackState(int surfaceId, @PlaybackState int playbackState, @Nullable Bundle playbackStateInfo)826         public void setPlaybackState(int surfaceId, @PlaybackState int playbackState,
827                 @Nullable Bundle playbackStateInfo) {
828             try {
829                 mCallback.setPlaybackState(surfaceId, playbackState, playbackStateInfo);
830             } catch (Exception e) {
831                 Log.w(TAG, "Failed to notify playback state (" + playbackState + ") for "
832                         + "surfaceId: " + surfaceId + " ; playbackStateInfo: " + playbackStateInfo,
833                         e);
834             }
835         }
836 
837         /**
838          * Returns the underliying {@link IBinder} object.
839          *
840          * @hide
841          */
getIBinder()842         public IBinder getIBinder() {
843             return mCallback.asBinder();
844         }
845     }
846 
847     /**
848      * {@link Binder} object backing a {@link CloudMediaSurfaceController} instance.
849      *
850      * @hide
851      */
852     public static class CloudMediaSurfaceControllerWrapper
853             extends ICloudMediaSurfaceController.Stub {
854 
855         final private CloudMediaSurfaceController mSurfaceController;
856 
CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController)857         CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController) {
858             mSurfaceController = surfaceController;
859         }
860 
861         @Override
onPlayerCreate()862         public void onPlayerCreate() {
863             Log.i(TAG, "Creating player.");
864             mSurfaceController.onPlayerCreate();
865         }
866 
867         @Override
onPlayerRelease()868         public void onPlayerRelease() {
869             Log.i(TAG, "Releasing player.");
870             mSurfaceController.onPlayerRelease();
871         }
872 
873         @Override
onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)874         public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
875                 @NonNull String mediaId) {
876             Log.i(TAG, "Surface prepared. SurfaceId: " + surfaceId + ". MediaId: " + mediaId);
877             mSurfaceController.onSurfaceCreated(surfaceId, surface, mediaId);
878         }
879 
880         @Override
onSurfaceChanged(int surfaceId, int format, int width, int height)881         public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
882             Log.i(TAG, "Surface changed. SurfaceId: " + surfaceId + ". Format: " + format
883                     + ". Width: " + width + ". Height: " + height);
884             mSurfaceController.onSurfaceChanged(surfaceId, format, width, height);
885         }
886 
887         @Override
onSurfaceDestroyed(int surfaceId)888         public void onSurfaceDestroyed(int surfaceId) {
889             Log.i(TAG, "Surface released. SurfaceId: " + surfaceId);
890             mSurfaceController.onSurfaceDestroyed(surfaceId);
891         }
892 
893         @Override
onMediaPlay(int surfaceId)894         public void onMediaPlay(int surfaceId) {
895             Log.i(TAG, "Media played. SurfaceId: " + surfaceId);
896             mSurfaceController.onMediaPlay(surfaceId);
897         }
898 
899         @Override
onMediaPause(int surfaceId)900         public void onMediaPause(int surfaceId) {
901             Log.i(TAG, "Media paused. SurfaceId: " + surfaceId);
902             mSurfaceController.onMediaPause(surfaceId);
903         }
904 
905         @Override
onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)906         public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
907             Log.i(TAG, "Media seeked. SurfaceId: " + surfaceId + ". Seek timestamp(ms): "
908                     + timestampMillis);
909             mSurfaceController.onMediaSeekTo(surfaceId, timestampMillis);
910         }
911 
912         @Override
onConfigChange(@onNull Bundle config)913         public void onConfigChange(@NonNull Bundle config) {
914             Log.i(TAG, "Config changed. Updated config params: " + config);
915             mSurfaceController.onConfigChange(config);
916         }
917 
918         @Override
onDestroy()919         public void onDestroy() {
920             Log.i(TAG, "Controller destroyed");
921             mSurfaceController.onDestroy();
922         }
923     }
924 
925     /**
926      * @hide
927      */
928     private class AsyncContentProviderWrapper extends IAsyncContentProvider.Stub {
929 
930         @Override
openMedia(String mediaId, RemoteCallback remoteCallback)931         public void openMedia(String mediaId, RemoteCallback remoteCallback) {
932             try {
933                 ParcelFileDescriptor pfd = onOpenMedia(mediaId,/* extras */
934                         null,/* cancellationSignal */ null);
935                 sendResult(pfd, null, remoteCallback);
936             } catch (Exception e) {
937                 sendResult(null, e, remoteCallback);
938             }
939         }
940 
sendResult(ParcelFileDescriptor pfd, Throwable throwable, RemoteCallback remoteCallback)941         private void sendResult(ParcelFileDescriptor pfd, Throwable throwable,
942                 RemoteCallback remoteCallback) {
943             Bundle bundle = new Bundle();
944             if (pfd == null && throwable == null) {
945                 throw new IllegalStateException("Expected ParcelFileDescriptor or an exception.");
946             }
947             if (pfd != null) {
948                 bundle.putParcelable(EXTRA_FILE_DESCRIPTOR, pfd);
949             }
950             if (throwable != null) {
951                 bundle.putString(EXTRA_ERROR_MESSAGE, throwable.getMessage());
952             }
953             remoteCallback.sendResult(bundle);
954         }
955     }
956 }
957