/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.provider; import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER; import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY; import static android.provider.CloudMediaProviderContract.EXTRA_ERROR_MESSAGE; import static android.provider.CloudMediaProviderContract.EXTRA_FILE_DESCRIPTOR; import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED; import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB; import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER; import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED; import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK; import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER; import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER; import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO; import static android.provider.CloudMediaProviderContract.URI_PATH_ALBUM; import static android.provider.CloudMediaProviderContract.URI_PATH_DELETED_MEDIA; import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA; import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO; import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER; import android.annotation.DurationMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.content.pm.ProviderInfo; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.graphics.PixelFormat; import android.graphics.Point; import android.media.MediaPlayer; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteCallback; import android.util.DisplayMetrics; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import java.io.FileNotFoundException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** * Base class for a cloud media provider. A cloud media provider offers read-only access to durable * media files, specifically photos and videos stored on a local disk, or files in a cloud storage * service. To create a cloud media provider, extend this class, implement the abstract methods, * and add it to your manifest like this: * *
<manifest>
 *    ...
 *    <application>
 *        ...
 *        <provider
 *            android:name="com.example.MyCloudProvider"
 *            android:authorities="com.example.mycloudprovider"
 *            android:exported="true"
 *            android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
 *            <intent-filter>
 *                <action android:name="android.content.action.CLOUD_MEDIA_PROVIDER" />
 *            </intent-filter>
 *        </provider>
 *        ...
 *    </application>
 *</manifest>
*

* When defining your provider, you must protect it with the * {@link CloudMediaProviderContract#MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION}, which is a permission * only the system can obtain, trying to define an unprotected {@link CloudMediaProvider} will * result in a {@link SecurityException}. *

* Applications cannot use a cloud media provider directly; they must go through * {@link MediaStore#ACTION_PICK_IMAGES} which requires a user to actively navigate and select * media items. When a user selects a media item through that UI, the system issues narrow URI * permission grants to the requesting application. *

Media items

*

* A media item must be an openable stream (with a specific MIME type). Media items can belong to * zero or more albums. Albums cannot contain other albums. *

* Each item under a provider is uniquely referenced by its media or album id, which must not * change which must be unique across all collection IDs as returned by * {@link #onGetMediaCollectionInfo}. * * @see MediaStore#ACTION_PICK_IMAGES */ public abstract class CloudMediaProvider extends ContentProvider { private static final String TAG = "CloudMediaProvider"; private static final int MATCH_MEDIAS = 1; private static final int MATCH_DELETED_MEDIAS = 2; private static final int MATCH_ALBUMS = 3; private static final int MATCH_MEDIA_COLLECTION_INFO = 4; private static final int MATCH_SURFACE_CONTROLLER = 5; private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true; private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false; private final UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH); private volatile int mMediaStoreAuthorityAppId; private String mAuthority; /** * Implementation is provided by the parent class. Cannot be overridden. */ @Override public final void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) { registerAuthority(info.authority); super.attachInfo(context, info); } private void registerAuthority(String authority) { mAuthority = authority; mMatcher.addURI(authority, URI_PATH_MEDIA, MATCH_MEDIAS); mMatcher.addURI(authority, URI_PATH_DELETED_MEDIA, MATCH_DELETED_MEDIAS); mMatcher.addURI(authority, URI_PATH_ALBUM, MATCH_ALBUMS); mMatcher.addURI(authority, URI_PATH_MEDIA_COLLECTION_INFO, MATCH_MEDIA_COLLECTION_INFO); mMatcher.addURI(authority, URI_PATH_SURFACE_CONTROLLER, MATCH_SURFACE_CONTROLLER); } /** * Returns {@link Bundle} containing binder to {@link IAsyncContentProvider}. * * @hide */ @NonNull public final Bundle onGetAsyncContentProvider() { Bundle bundle = new Bundle(); bundle.putBinder(EXTRA_ASYNC_CONTENT_PROVIDER, (new AsyncContentProviderWrapper()).asBinder()); return bundle; } /** * Returns metadata about the media collection itself. *

* This is useful for the OS to determine if its cache of media items in the collection is * still valid and if a full or incremental sync is required with {@link #onQueryMedia}. *

* This method might be called by the OS frequently and is performance critical, hence it should * avoid long running operations. *

* If the provider handled any filters in {@code extras}, it must add the key to the * {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned {@link Bundle}. * * @param extras containing keys to filter result: *

* * @return {@link Bundle} containing {@link CloudMediaProviderContract.MediaCollectionInfo} * */ @SuppressWarnings("unused") @NonNull public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras); /** * Returns a cursor representing all media items in the media collection optionally filtered by * {@code extras} and sorted in reverse chronological order of * {@link CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS}, i.e. most recent items * first. *

* The cloud media provider must set the * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the * returned {@link Cursor}. *

* If the cloud media provider handled any filters in {@code extras}, it must add the key to * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned * {@link Cursor#setExtras} {@link Bundle}. * * @param extras containing keys to filter media items: *

* @return cursor representing media items containing all * {@link CloudMediaProviderContract.MediaColumns} columns */ @SuppressWarnings("unused") @NonNull public abstract Cursor onQueryMedia(@NonNull Bundle extras); /** * Returns a {@link Cursor} representing all deleted media items in the entire media collection * within the current provider version as returned by {@link #onGetMediaCollectionInfo}. These * items can be optionally filtered by {@code extras}. *

* The cloud media provider must set the * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the * returned {@link Cursor}. *

* If the provider handled any filters in {@code extras}, it must add the key to * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned * {@link Cursor#setExtras} {@link Bundle}. * * @param extras containing keys to filter deleted media items: *

* @return cursor representing deleted media items containing just the * {@link CloudMediaProviderContract.MediaColumns#ID} column */ @SuppressWarnings("unused") @NonNull public abstract Cursor onQueryDeletedMedia(@NonNull Bundle extras); /** * Returns a cursor representing all album items in the media collection optionally filtered * by {@code extras} and sorted in reverse chronological order of * {@link CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}, i.e. most recent items * first. *

* The cloud media provider must set the * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the * returned {@link Cursor}. *

* If the provider handled any filters in {@code extras}, it must add the key to * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned * {@link Cursor#setExtras} {@link Bundle}. * * @param extras containing keys to filter album items: *

* @return cursor representing album items containing all * {@link CloudMediaProviderContract.AlbumColumns} columns */ @SuppressWarnings("unused") @NonNull public Cursor onQueryAlbums(@NonNull Bundle extras) { throw new UnsupportedOperationException("queryAlbums not supported"); } /** * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId} *

The cloud media provider should strictly return thumbnail in the original * {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item. *

* This is expected to be a much lower resolution version than the item returned by * {@link #onOpenMedia}. *

* If you block while downloading content, you should periodically check * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. * * @param mediaId the media item to return * @param size the dimensions of the thumbnail to return. The returned file descriptor doesn't * have to match the {@code size} precisely because the OS will adjust the dimensions before * usage. Implementations can return close approximations especially if the approximation is * already locally on the device and doesn't require downloading from the cloud. * @param extras to modify the way the fd is opened, e.g. for video files we may request a * thumbnail image instead of a video with * {@link CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL} * @param signal used by the OS to signal if the request should be cancelled * @return read-only file descriptor for accessing the thumbnail for the media file * * @see #onOpenMedia * @see CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL */ @SuppressWarnings("unused") @NonNull public abstract AssetFileDescriptor onOpenPreview(@NonNull String mediaId, @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal) throws FileNotFoundException; /** * Returns the full size media item identified by {@code mediaId}. *

* If you block while downloading content, you should periodically check * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. * * @param mediaId the media item to return * @param extras to modify the way the fd is opened, there's none at the moment, but some * might be implemented in the future * @param signal used by the OS to signal if the request should be cancelled * @return read-only file descriptor for accessing the media file * * @see #onOpenPreview */ @SuppressWarnings("unused") @NonNull public abstract ParcelFileDescriptor onOpenMedia(@NonNull String mediaId, @Nullable Bundle extras, @Nullable CancellationSignal signal) throws FileNotFoundException; /** * Returns a {@link CloudMediaSurfaceController} used for rendering the preview of media items, * or null if preview rendering is not supported. * * @param config containing configuration parameters for {@link CloudMediaSurfaceController} *

* @param callback {@link CloudMediaSurfaceStateChangedCallback} to send state updates for * {@link Surface} to picker launched via {@link MediaStore#ACTION_PICK_IMAGES} */ @Nullable public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config, @NonNull CloudMediaSurfaceStateChangedCallback callback) { return null; } /** * Implementation is provided by the parent class. Cannot be overridden. */ @Override @NonNull public final Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { if (!method.startsWith("android:")) { // Ignore non-platform methods return super.call(method, arg, extras); } try { return callUnchecked(method, arg, extras); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } private Bundle callUnchecked(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) throws FileNotFoundException { if (extras == null) { extras = new Bundle(); } Bundle result = new Bundle(); if (METHOD_GET_MEDIA_COLLECTION_INFO.equals(method)) { long startTime = System.currentTimeMillis(); result = onGetMediaCollectionInfo(extras); CmpApiVerifier.verifyApiResult(new CmpApiResult( CmpApiVerifier.CloudMediaProviderApis.OnGetMediaCollectionInfo, result), System.currentTimeMillis() - startTime, mAuthority); } else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) { result = onCreateCloudMediaSurfaceController(extras); } else if (METHOD_GET_ASYNC_CONTENT_PROVIDER.equals(method)) { result = onGetAsyncContentProvider(); } else { throw new UnsupportedOperationException("Method not supported " + method); } return result; } private Bundle onCreateCloudMediaSurfaceController(@NonNull Bundle extras) { Objects.requireNonNull(extras); final IBinder binder = extras.getBinder(EXTRA_SURFACE_STATE_CALLBACK); if (binder == null) { throw new IllegalArgumentException("Missing surface state callback"); } final boolean enableLoop = extras.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, DEFAULT_LOOPING_PLAYBACK_ENABLED); final boolean muteAudio = extras.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED); final String authority = extras.getString(EXTRA_AUTHORITY); final CloudMediaSurfaceStateChangedCallback callback = new CloudMediaSurfaceStateChangedCallback( ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder)); final Bundle config = new Bundle(); config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, enableLoop); config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, muteAudio); config.putString(EXTRA_AUTHORITY, authority); final CloudMediaSurfaceController controller = onCreateCloudMediaSurfaceController(config, callback); if (controller == null) { Log.d(TAG, "onCreateCloudMediaSurfaceController returned null"); return Bundle.EMPTY; } Bundle result = new Bundle(); result.putBinder(EXTRA_SURFACE_CONTROLLER, new CloudMediaSurfaceControllerWrapper(controller).asBinder()); return result; } /** * Implementation is provided by the parent class. Cannot be overridden. * * @see #onOpenMedia */ @NonNull @Override public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { return openFile(uri, mode, null); } /** * Implementation is provided by the parent class. Cannot be overridden. * * @see #onOpenMedia */ @NonNull @Override public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal) throws FileNotFoundException { String mediaId = uri.getLastPathSegment(); long startTime = System.currentTimeMillis(); ParcelFileDescriptor result = onOpenMedia(mediaId, /* extras */ null, signal); CmpApiVerifier.verifyApiResult(new CmpApiResult( CmpApiVerifier.CloudMediaProviderApis.OnOpenMedia, result), System.currentTimeMillis() - startTime, mAuthority); return result; } /** * Implementation is provided by the parent class. Cannot be overridden. * * @see #onOpenPreview * @see #onOpenMedia */ @NonNull @Override public final AssetFileDescriptor openTypedAssetFile(@NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts) throws FileNotFoundException { return openTypedAssetFile(uri, mimeTypeFilter, opts, null); } /** * Implementation is provided by the parent class. Cannot be overridden. * * @see #onOpenPreview * @see #onOpenMedia */ @NonNull @Override public final AssetFileDescriptor openTypedAssetFile( @NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts, @Nullable CancellationSignal signal) throws FileNotFoundException { final String mediaId = uri.getLastPathSegment(); final Bundle bundle = new Bundle(); Point previewSize = null; final DisplayMetrics screenMetrics = getContext().getResources().getDisplayMetrics(); int minPreviewLength = Math.min(screenMetrics.widthPixels, screenMetrics.heightPixels); if (opts != null) { bundle.putBoolean(EXTRA_MEDIASTORE_THUMB, opts.getBoolean(EXTRA_MEDIASTORE_THUMB)); if (opts.containsKey(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)) { bundle.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true); minPreviewLength = minPreviewLength / 2; } previewSize = opts.getParcelable(ContentResolver.EXTRA_SIZE); } if (previewSize == null) { previewSize = new Point(minPreviewLength, minPreviewLength); } long startTime = System.currentTimeMillis(); AssetFileDescriptor result = onOpenPreview(mediaId, previewSize, bundle, signal); CmpApiVerifier.verifyApiResult(new CmpApiResult( CmpApiVerifier.CloudMediaProviderApis.OnOpenPreview, result, previewSize), System.currentTimeMillis() - startTime, mAuthority); return result; } /** * Implementation is provided by the parent class. Cannot be overridden. * * @see #onQueryMedia * @see #onQueryDeletedMedia * @see #onQueryAlbums */ @NonNull @Override public final Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) { if (queryArgs == null) { queryArgs = new Bundle(); } Cursor result; long startTime = System.currentTimeMillis(); switch (mMatcher.match(uri)) { case MATCH_MEDIAS: result = onQueryMedia(queryArgs); CmpApiVerifier.verifyApiResult(new CmpApiResult( CmpApiVerifier.CloudMediaProviderApis.OnQueryMedia, result), System.currentTimeMillis() - startTime, mAuthority); break; case MATCH_DELETED_MEDIAS: result = onQueryDeletedMedia(queryArgs); CmpApiVerifier.verifyApiResult(new CmpApiResult( CmpApiVerifier.CloudMediaProviderApis.OnQueryDeletedMedia, result), System.currentTimeMillis() - startTime, mAuthority); break; case MATCH_ALBUMS: result = onQueryAlbums(queryArgs); CmpApiVerifier.verifyApiResult(new CmpApiResult( CmpApiVerifier.CloudMediaProviderApis.OnQueryAlbums, result), System.currentTimeMillis() - startTime, mAuthority); break; default: throw new UnsupportedOperationException("Unsupported Uri " + uri); } return result; } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @NonNull @Override public final String getType(@NonNull Uri uri) { throw new UnsupportedOperationException("getType not supported"); } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @NonNull @Override public final Uri canonicalize(@NonNull Uri uri) { throw new UnsupportedOperationException("Canonicalize not supported"); } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @NonNull @Override public final Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary // transport method. We override that, and don't ever delegate to this method. throw new UnsupportedOperationException("Pre-Android-O query format not supported."); } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @NonNull @Override public final Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) { // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary // transport method. We override that, and don't ever delegate to this metohd. throw new UnsupportedOperationException("Pre-Android-O query format not supported."); } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @NonNull @Override public final Uri insert(@NonNull Uri uri, @NonNull ContentValues values) { throw new UnsupportedOperationException("Insert not supported"); } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @Override public final int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { throw new UnsupportedOperationException("Delete not supported"); } /** * Implementation is provided by the parent class. Throws by default, and * cannot be overridden. */ @Override public final int update(@NonNull Uri uri, @NonNull ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { throw new UnsupportedOperationException("Update not supported"); } /** * Manages rendering the preview of media items on given instances of {@link Surface}. * *

The methods of this class are meant to be asynchronous, and should not block by performing * any heavy operation. *

Note that a single CloudMediaSurfaceController instance would be responsible for * rendering multiple media items associated with multiple surfaces. */ @SuppressLint("PackageLayering") // We need to pass in a Surface which can be prepared for // rendering a media item. public static abstract class CloudMediaSurfaceController { /** * Creates any player resource(s) needed for rendering. */ public abstract void onPlayerCreate(); /** * Releases any player resource(s) used for rendering. */ public abstract void onPlayerRelease(); /** * Indicates creation of the given {@link Surface} with given {@code surfaceId} for * rendering the preview of a media item with given {@code mediaId}. * *

This is called immediately after the surface is first created. Implementations of this * should start up whatever rendering code they desire. *

Note that the given media item remains associated with the given surface id till the * {@link Surface} is destroyed. * * @param surfaceId id which uniquely identifies the {@link Surface} for rendering * @param surface instance of the {@link Surface} on which the media item should be rendered * @param mediaId id which uniquely identifies the media to be rendered * * @see SurfaceHolder.Callback#surfaceCreated(SurfaceHolder) */ public abstract void onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId); /** * Indicates structural changes (format or size) in the {@link Surface} for rendering. * *

This method is always called at least once, after {@link #onSurfaceCreated}. * * @param surfaceId id which uniquely identifies the {@link Surface} for rendering * @param format the new {@link PixelFormat} of the surface * @param width the new width of the {@link Surface} * @param height the new height of the {@link Surface} * * @see SurfaceHolder.Callback#surfaceChanged(SurfaceHolder, int, int, int) */ public abstract void onSurfaceChanged(int surfaceId, int format, int width, int height); /** * Indicates destruction of a {@link Surface} with given {@code surfaceId}. * *

This is called immediately before a surface is being destroyed. After returning from * this call, you should no longer try to access this surface. * * @param surfaceId id which uniquely identifies the {@link Surface} for rendering * * @see SurfaceHolder.Callback#surfaceDestroyed(SurfaceHolder) */ public abstract void onSurfaceDestroyed(int surfaceId); /** * Start playing the preview of the media associated with the given surface id. If * playback had previously been paused, playback will continue from where it was paused. * If playback had been stopped, or never started before, playback will start at the * beginning. * * @param surfaceId id which uniquely identifies the {@link Surface} for rendering */ public abstract void onMediaPlay(int surfaceId); /** * Pauses the playback of the media associated with the given surface id. * * @param surfaceId id which uniquely identifies the {@link Surface} for rendering */ public abstract void onMediaPause(int surfaceId); /** * Seeks the media associated with the given surface id to specified timestamp. * * @param surfaceId id which uniquely identifies the {@link Surface} for rendering * @param timestampMillis the timestamp in milliseconds from the start to seek to */ public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis); /** * Changes the configuration parameters for the CloudMediaSurfaceController. * * @param config the updated config to change to. This can include config changes for the * following: *

*/ public abstract void onConfigChange(@NonNull Bundle config); /** * Indicates destruction of this CloudMediaSurfaceController object. * *

This CloudMediaSurfaceController object should no longer be in use after this method * has been called. * *

Note that it is possible for this method to be called directly without * {@link #onPlayerRelease} being called, hence you should release any resources associated * with this CloudMediaSurfaceController object, or perform any cleanup required in this * method. */ public abstract void onDestroy(); } /** * This class is used by {@link CloudMediaProvider} to send {@link Surface} state updates to * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}. * * @see MediaStore#ACTION_PICK_IMAGES */ public static final class CloudMediaSurfaceStateChangedCallback { /** {@hide} */ @IntDef(flag = true, prefix = { "PLAYBACK_STATE_" }, value = { PLAYBACK_STATE_BUFFERING, PLAYBACK_STATE_READY, PLAYBACK_STATE_STARTED, PLAYBACK_STATE_PAUSED, PLAYBACK_STATE_COMPLETED, PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE, PLAYBACK_STATE_ERROR_PERMANENT_FAILURE, PLAYBACK_STATE_MEDIA_SIZE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface PlaybackState {} /** * Constant to notify that the playback is buffering */ public static final int PLAYBACK_STATE_BUFFERING = 1; /** * Constant to notify that the playback is ready to be played */ public static final int PLAYBACK_STATE_READY = 2; /** * Constant to notify that the playback has started */ public static final int PLAYBACK_STATE_STARTED = 3; /** * Constant to notify that the playback is paused. */ public static final int PLAYBACK_STATE_PAUSED = 4; /** * Constant to notify that the playback has completed */ public static final int PLAYBACK_STATE_COMPLETED = 5; /** * Constant to notify that the playback has failed with a retriable error. */ public static final int PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE = 6; /** * Constant to notify that the playback has failed with a permanent error. */ public static final int PLAYBACK_STATE_ERROR_PERMANENT_FAILURE = 7; /** * Constant to notify that the media size is first known or has changed. * * Pass the width and height of the media as a {@link Point} inside the {@link Bundle} with * {@link ContentResolver#EXTRA_SIZE} as the key. * * @see CloudMediaSurfaceStateChangedCallback#setPlaybackState(int, int, Bundle) * @see MediaPlayer.OnVideoSizeChangedListener#onVideoSizeChanged(MediaPlayer, int, int) */ public static final int PLAYBACK_STATE_MEDIA_SIZE_CHANGED = 8; private final ICloudMediaSurfaceStateChangedCallback mCallback; CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback) { mCallback = callback; } /** * This is called to notify playback state update for a {@link Surface} * on the picker launched via {@link MediaStore#ACTION_PICK_IMAGES}. * * @param surfaceId id which uniquely identifies a {@link Surface} * @param playbackState playback state to notify picker about * @param playbackStateInfo {@link Bundle} which may contain extra information about the * playback state, such as media size, progress/seek info or * details about errors. */ public void setPlaybackState(int surfaceId, @PlaybackState int playbackState, @Nullable Bundle playbackStateInfo) { try { mCallback.setPlaybackState(surfaceId, playbackState, playbackStateInfo); } catch (Exception e) { Log.w(TAG, "Failed to notify playback state (" + playbackState + ") for " + "surfaceId: " + surfaceId + " ; playbackStateInfo: " + playbackStateInfo, e); } } /** * Returns the underliying {@link IBinder} object. * * @hide */ public IBinder getIBinder() { return mCallback.asBinder(); } } /** * {@link Binder} object backing a {@link CloudMediaSurfaceController} instance. * * @hide */ public static class CloudMediaSurfaceControllerWrapper extends ICloudMediaSurfaceController.Stub { final private CloudMediaSurfaceController mSurfaceController; CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController) { mSurfaceController = surfaceController; } @Override public void onPlayerCreate() { Log.i(TAG, "Creating player."); mSurfaceController.onPlayerCreate(); } @Override public void onPlayerRelease() { Log.i(TAG, "Releasing player."); mSurfaceController.onPlayerRelease(); } @Override public void onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId) { Log.i(TAG, "Surface prepared. SurfaceId: " + surfaceId + ". MediaId: " + mediaId); mSurfaceController.onSurfaceCreated(surfaceId, surface, mediaId); } @Override public void onSurfaceChanged(int surfaceId, int format, int width, int height) { Log.i(TAG, "Surface changed. SurfaceId: " + surfaceId + ". Format: " + format + ". Width: " + width + ". Height: " + height); mSurfaceController.onSurfaceChanged(surfaceId, format, width, height); } @Override public void onSurfaceDestroyed(int surfaceId) { Log.i(TAG, "Surface released. SurfaceId: " + surfaceId); mSurfaceController.onSurfaceDestroyed(surfaceId); } @Override public void onMediaPlay(int surfaceId) { Log.i(TAG, "Media played. SurfaceId: " + surfaceId); mSurfaceController.onMediaPlay(surfaceId); } @Override public void onMediaPause(int surfaceId) { Log.i(TAG, "Media paused. SurfaceId: " + surfaceId); mSurfaceController.onMediaPause(surfaceId); } @Override public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) { Log.i(TAG, "Media seeked. SurfaceId: " + surfaceId + ". Seek timestamp(ms): " + timestampMillis); mSurfaceController.onMediaSeekTo(surfaceId, timestampMillis); } @Override public void onConfigChange(@NonNull Bundle config) { Log.i(TAG, "Config changed. Updated config params: " + config); mSurfaceController.onConfigChange(config); } @Override public void onDestroy() { Log.i(TAG, "Controller destroyed"); mSurfaceController.onDestroy(); } } /** * @hide */ private class AsyncContentProviderWrapper extends IAsyncContentProvider.Stub { @Override public void openMedia(String mediaId, RemoteCallback remoteCallback) { try { ParcelFileDescriptor pfd = onOpenMedia(mediaId,/* extras */ null,/* cancellationSignal */ null); sendResult(pfd, null, remoteCallback); } catch (Exception e) { sendResult(null, e, remoteCallback); } } private void sendResult(ParcelFileDescriptor pfd, Throwable throwable, RemoteCallback remoteCallback) { Bundle bundle = new Bundle(); if (pfd == null && throwable == null) { throw new IllegalStateException("Expected ParcelFileDescriptor or an exception."); } if (pfd != null) { bundle.putParcelable(EXTRA_FILE_DESCRIPTOR, pfd); } if (throwable != null) { bundle.putString(EXTRA_ERROR_MESSAGE, throwable.getMessage()); } remoteCallback.sendResult(bundle); } } }