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