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