953 lines
40 KiB
Java
953 lines
40 KiB
Java
/*
|
|
* 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:
|
|
*
|
|
* <pre class="prettyprint"><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></pre>
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* 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.
|
|
* <h3>Media items</h3>
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* This method might be called by the OS frequently and is performance critical, hence it should
|
|
* avoid long running operations.
|
|
* <p>
|
|
* 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:
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
|
|
* </ul>
|
|
*
|
|
* @return {@link Bundle} containing {@link CloudMediaProviderContract.MediaCollectionInfo}
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}
|
|
* <li> {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}
|
|
* <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}
|
|
* <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}
|
|
* </ul>
|
|
*/
|
|
@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.
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* 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:
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
|
|
* </ul>
|
|
* @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}.
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* 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:
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
|
|
* </ul>
|
|
* @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.
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* 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:
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
|
|
* </ul>
|
|
* @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}
|
|
* <p>The cloud media provider should strictly return thumbnail in the original
|
|
* {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item.
|
|
* <p>
|
|
* This is expected to be a much lower resolution version than the item returned by
|
|
* {@link #onOpenMedia}.
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* 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}
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
|
|
* </ul>
|
|
* @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(String method, String arg, Bundle extras)
|
|
throws FileNotFoundException {
|
|
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}.
|
|
*
|
|
* <p>The methods of this class are meant to be asynchronous, and should not block by performing
|
|
* any heavy operation.
|
|
* <p>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}.
|
|
*
|
|
* <p>This is called immediately after the surface is first created. Implementations of this
|
|
* should start up whatever rendering code they desire.
|
|
* <p>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.
|
|
*
|
|
* <p>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}.
|
|
*
|
|
* <p>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:
|
|
* <ul>
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
|
|
* <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
|
|
* </ul>
|
|
*/
|
|
public abstract void onConfigChange(@NonNull Bundle config);
|
|
|
|
/**
|
|
* Indicates destruction of this CloudMediaSurfaceController object.
|
|
*
|
|
* <p>This CloudMediaSurfaceController object should no longer be in use after this method
|
|
* has been called.
|
|
*
|
|
* <p>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);
|
|
}
|
|
}
|
|
}
|