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_PROVIDER_CAPABILITIES; 26 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER; 27 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED; 28 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK; 29 import static android.provider.CloudMediaProviderContract.KEY_MEDIA_CATEGORY_ID; 30 import static android.provider.CloudMediaProviderContract.KEY_MEDIA_SET_ID; 31 import static android.provider.CloudMediaProviderContract.KEY_PARENT_CATEGORY_ID; 32 import static android.provider.CloudMediaProviderContract.KEY_PREFIX_TEXT; 33 import static android.provider.CloudMediaProviderContract.KEY_SEARCH_TEXT; 34 import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER; 35 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER; 36 import static android.provider.CloudMediaProviderContract.METHOD_GET_CAPABILITIES; 37 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO; 38 import static android.provider.CloudMediaProviderContract.URI_PATH_ALBUM; 39 import static android.provider.CloudMediaProviderContract.URI_PATH_DELETED_MEDIA; 40 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA; 41 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY; 42 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO; 43 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_SET; 44 import static android.provider.CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET; 45 import static android.provider.CloudMediaProviderContract.URI_PATH_SEARCH_MEDIA; 46 import static android.provider.CloudMediaProviderContract.URI_PATH_SEARCH_SUGGESTION; 47 import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER; 48 49 import android.annotation.DurationMillisLong; 50 import android.annotation.FlaggedApi; 51 import android.annotation.IntDef; 52 import android.annotation.NonNull; 53 import android.annotation.Nullable; 54 import android.annotation.SuppressLint; 55 import android.content.ContentProvider; 56 import android.content.ContentResolver; 57 import android.content.ContentValues; 58 import android.content.Context; 59 import android.content.UriMatcher; 60 import android.content.pm.ProviderInfo; 61 import android.content.res.AssetFileDescriptor; 62 import android.database.Cursor; 63 import android.graphics.PixelFormat; 64 import android.graphics.Point; 65 import android.media.MediaPlayer; 66 import android.net.Uri; 67 import android.os.Binder; 68 import android.os.Bundle; 69 import android.os.CancellationSignal; 70 import android.os.IBinder; 71 import android.os.ParcelFileDescriptor; 72 import android.os.RemoteCallback; 73 import android.util.DisplayMetrics; 74 import android.util.Log; 75 import android.view.Surface; 76 import android.view.SurfaceHolder; 77 78 import com.android.providers.media.flags.Flags; 79 80 import java.io.FileNotFoundException; 81 import java.lang.annotation.Retention; 82 import java.lang.annotation.RetentionPolicy; 83 import java.util.Objects; 84 85 /** 86 * Base class for a cloud media provider. A cloud media provider offers read-only access to durable 87 * media files, specifically photos and videos stored on a local disk, or files in a cloud storage 88 * service. To create a cloud media provider, extend this class, implement the abstract methods, 89 * and add it to your manifest like this: 90 * 91 * <pre class="prettyprint"><manifest> 92 * ... 93 * <application> 94 * ... 95 * <provider 96 * android:name="com.example.MyCloudProvider" 97 * android:authorities="com.example.mycloudprovider" 98 * android:exported="true" 99 * android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS" 100 * <intent-filter> 101 * <action android:name="android.content.action.CLOUD_MEDIA_PROVIDER" /> 102 * </intent-filter> 103 * </provider> 104 * ... 105 * </application> 106 *</manifest></pre> 107 * <p> 108 * When defining your provider, you must protect it with the 109 * {@link CloudMediaProviderContract#MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION}, which is a permission 110 * only the system can obtain, trying to define an unprotected {@link CloudMediaProvider} will 111 * result in a {@link SecurityException}. 112 * <p> 113 * Applications cannot use a cloud media provider directly; they must go through 114 * {@link MediaStore#ACTION_PICK_IMAGES} which requires a user to actively navigate and select 115 * media items. When a user selects a media item through that UI, the system issues narrow URI 116 * permission grants to the requesting application. 117 * <h3>Media items</h3> 118 * <p> 119 * A media item must be an openable stream (with a specific MIME type). Media items can belong to 120 * zero or more albums. Albums cannot contain other albums. 121 * <p> 122 * Each item under a provider is uniquely referenced by its media or album id, which must not 123 * change which must be unique across all collection IDs as returned by 124 * {@link #onGetMediaCollectionInfo}. 125 * 126 * @see MediaStore#ACTION_PICK_IMAGES 127 */ 128 public abstract class CloudMediaProvider extends ContentProvider { 129 private static final String TAG = "CloudMediaProvider"; 130 131 private static final int MATCH_MEDIAS = 1; 132 private static final int MATCH_DELETED_MEDIAS = 2; 133 private static final int MATCH_ALBUMS = 3; 134 private static final int MATCH_MEDIA_COLLECTION_INFO = 4; 135 private static final int MATCH_SURFACE_CONTROLLER = 5; 136 private static final int MATCH_MEDIA_CATEGORIES = 6; 137 private static final int MATCH_MEDIA_SETS = 7; 138 private static final int MATCH_SEARCH_SUGGESTION = 8; 139 private static final int MATCH_SEARCH = 9; 140 private static final int MATCH_MEDIAS_IN_MEDIA_SET = 10; 141 142 private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true; 143 private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false; 144 145 private final UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 146 private volatile int mMediaStoreAuthorityAppId; 147 148 private String mAuthority; 149 150 151 /** 152 * Implementation is provided by the parent class. Cannot be overridden. 153 */ 154 @Override attachInfo(@onNull Context context, @NonNull ProviderInfo info)155 public final void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) { 156 registerAuthority(info.authority); 157 158 super.attachInfo(context, info); 159 } 160 registerAuthority(String authority)161 private void registerAuthority(String authority) { 162 mAuthority = authority; 163 mMatcher.addURI(authority, URI_PATH_MEDIA, MATCH_MEDIAS); 164 mMatcher.addURI(authority, URI_PATH_DELETED_MEDIA, MATCH_DELETED_MEDIAS); 165 mMatcher.addURI(authority, URI_PATH_ALBUM, MATCH_ALBUMS); 166 mMatcher.addURI(authority, URI_PATH_MEDIA_COLLECTION_INFO, MATCH_MEDIA_COLLECTION_INFO); 167 mMatcher.addURI(authority, URI_PATH_SURFACE_CONTROLLER, MATCH_SURFACE_CONTROLLER); 168 mMatcher.addURI(authority, URI_PATH_MEDIA_CATEGORY, MATCH_MEDIA_CATEGORIES); 169 mMatcher.addURI(authority, URI_PATH_MEDIA_SET, MATCH_MEDIA_SETS); 170 mMatcher.addURI(authority, URI_PATH_SEARCH_SUGGESTION, MATCH_SEARCH_SUGGESTION); 171 mMatcher.addURI(authority, URI_PATH_SEARCH_MEDIA, MATCH_SEARCH); 172 mMatcher.addURI(authority, URI_PATH_MEDIA_IN_MEDIA_SET, MATCH_MEDIAS_IN_MEDIA_SET); 173 } 174 175 /** 176 * Returns {@link Bundle} containing binder to {@link IAsyncContentProvider}. 177 * 178 * @hide 179 */ 180 @NonNull onGetAsyncContentProvider()181 public final Bundle onGetAsyncContentProvider() { 182 Bundle bundle = new Bundle(); 183 bundle.putBinder(EXTRA_ASYNC_CONTENT_PROVIDER, 184 (new AsyncContentProviderWrapper()).asBinder()); 185 return bundle; 186 } 187 188 /** 189 * Returns the {@link CloudMediaProviderContract.Capabilities} of this 190 * CloudMediaProvider. 191 * 192 * This object is used to determine which APIs can be safely invoked during 193 * runtime. 194 * 195 * If not overridden the default capabilities are used. 196 * 197 * IMPORTANT: This method is performance critical and should avoid long running 198 * or expensive operations. 199 * 200 * @see CloudMediaProviderContract.Capabilities 201 * 202 */ 203 @NonNull 204 @FlaggedApi(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES) onGetCapabilities()205 public CloudMediaProviderContract.Capabilities onGetCapabilities() { 206 return new CloudMediaProviderContract.Capabilities.Builder().build(); 207 } 208 209 /** 210 * Returns metadata about the media collection itself. 211 * <p> 212 * This is useful for the OS to determine if its cache of media items in the collection is 213 * still valid and if a full or incremental sync is required with {@link #onQueryMedia}. 214 * <p> 215 * This method might be called by the OS frequently and is performance critical, hence it should 216 * avoid long running operations. 217 * <p> 218 * If the provider handled any filters in {@code extras}, it must add the key to the 219 * {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned {@link Bundle}. 220 * 221 * @param extras containing keys to filter result: 222 * <ul> 223 * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID} 224 * </ul> 225 * 226 * @return {@link Bundle} containing {@link CloudMediaProviderContract.MediaCollectionInfo} 227 * <ul> 228 * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID} 229 * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION} 230 * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME} 231 * <li> {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT} 232 * </ul> 233 */ 234 @SuppressWarnings("unused") 235 @NonNull onGetMediaCollectionInfo(@onNull Bundle extras)236 public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras); 237 238 /** 239 * Returns a cursor representing all media items in the media collection optionally filtered by 240 * {@code extras} and sorted in reverse chronological order of 241 * {@link CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS}, i.e. most recent items 242 * first. 243 * <p> 244 * The cloud media provider must set the 245 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned 246 * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the 247 * returned {@link Cursor}. 248 * <p> 249 * If the cloud media provider handled any filters in {@code extras}, it must add the key to 250 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned 251 * {@link Cursor#setExtras} {@link Bundle}. 252 * 253 * @param extras containing keys to filter media items: 254 * <ul> 255 * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION} 256 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 257 * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID} 258 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 259 * </ul> 260 * @return cursor representing media items containing all 261 * {@link CloudMediaProviderContract.MediaColumns} columns 262 */ 263 @SuppressWarnings("unused") 264 @NonNull onQueryMedia(@onNull Bundle extras)265 public abstract Cursor onQueryMedia(@NonNull Bundle extras); 266 267 /** 268 * Returns a {@link Cursor} representing all deleted media items in the entire media collection 269 * within the current provider version as returned by {@link #onGetMediaCollectionInfo}. These 270 * items can be optionally filtered by {@code extras}. 271 * <p> 272 * The cloud media provider must set the 273 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned 274 * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the 275 * returned {@link Cursor}. 276 * <p> 277 * If the provider handled any filters in {@code extras}, it must add the key to 278 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned 279 * {@link Cursor#setExtras} {@link Bundle}. 280 * 281 * @param extras containing keys to filter deleted media items: 282 * <ul> 283 * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION} 284 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 285 * </ul> 286 * @return cursor representing deleted media items containing just the 287 * {@link CloudMediaProviderContract.MediaColumns#ID} column 288 */ 289 @SuppressWarnings("unused") 290 @NonNull onQueryDeletedMedia(@onNull Bundle extras)291 public abstract Cursor onQueryDeletedMedia(@NonNull Bundle extras); 292 293 /** 294 * Returns a cursor representing all album items in the media collection optionally filtered 295 * by {@code extras} and sorted in reverse chronological order of 296 * {@link CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}, i.e. most recent items 297 * first. 298 * <p> 299 * The cloud media provider must set the 300 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned 301 * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the 302 * returned {@link Cursor}. 303 * 304 * <p> 305 * If the provider handled any filters in {@code extras}, it must add the key to 306 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned 307 * {@link Cursor#setExtras} {@link Bundle}. 308 * 309 * @param extras containing keys to filter album items: 310 * <ul> 311 * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION} 312 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 313 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 314 * <li> {@link android.content.Intent#EXTRA_MIME_TYPES} 315 * </ul> 316 * @return cursor representing album items containing all 317 * {@link CloudMediaProviderContract.AlbumColumns} columns 318 */ 319 @SuppressWarnings("unused") 320 @NonNull onQueryAlbums(@onNull Bundle extras)321 public Cursor onQueryAlbums(@NonNull Bundle extras) { 322 throw new UnsupportedOperationException("queryAlbums not supported"); 323 } 324 325 /** 326 * Queries for the available MediaCategories under the given {@code parentCategoryId}, 327 * filtered by {@code extras}. The columns of MediaCategories are 328 * in the class {@link CloudMediaProviderContract.MediaCategoryColumns}. 329 * 330 * <p> 331 * When {@code parentCategoryId} is null, this returns the root categories. 332 * 333 * <p> 334 * The order in which media categories are sorted in the cursor 335 * will be retained when displaying results to the user. 336 * 337 * <p> 338 * The cloud media provider must set the 339 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor 340 * by using {@link Cursor#setExtras}. Not setting this is an error and invalidates the 341 * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. 342 * 343 * <p> 344 * {@code extras} may contain some key-value pairs which should be used to filter the results. 345 * If the provider handled any filters in {@code extras}, it must add the key to 346 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using 347 * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is 348 * without the extra being used. 349 * Note: Currently this function does not pass any params in {@code extras}. 350 * 351 * @param parentCategoryId the ID of the parent category to filter media categories. 352 * @param extras containing keys to filter media categories: 353 * <ul> 354 * <li> {@link android.content.Intent#EXTRA_MIME_TYPES} 355 * </ul> 356 * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. 357 * @return cursor with {@link CloudMediaProviderContract.MediaCategoryColumns} columns 358 */ 359 @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) 360 @NonNull onQueryMediaCategories(@ullable String parentCategoryId, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)361 public Cursor onQueryMediaCategories(@Nullable String parentCategoryId, 362 @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { 363 throw new UnsupportedOperationException("onQueryMediaCategories is not supported"); 364 } 365 366 /** 367 * Queries for the available MediaSets under a given {@code mediaCategoryId}, 368 * filtered by {@code extras}. The columns of MediaSet are in the class 369 * {@link CloudMediaProviderContract.MediaSetColumns}. 370 * 371 * <p> 372 * This returns MediaSets directly inside the given MediaCategoryId. 373 * If the passed mediaCategoryId has some more nested mediaCategories, the mediaSets inside 374 * the nested mediaCategories must not be returned in this response. 375 * 376 * <p> 377 * The order in which media sets are sorted in the cursor 378 * will be retained when displaying results to the user. 379 * 380 * <p> 381 * The cloud media provider must set the 382 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor 383 * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the 384 * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. 385 * 386 * <p> 387 * {@code extras} may contain some key-value pairs which should be used to prepare the results. 388 * If the provider handled any filters in {@code extras}, it must add the key to 389 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using 390 * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is 391 * without the extra being used. 392 * 393 * <p> 394 * If the cloud media provider supports pagination, they can set 395 * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, 396 * as part of the returned cursor by using {@link Cursor#setExtras}. 397 * If a token is set, the OS will pass it as a key-value pair in {@code extras} 398 * when querying for query media sets for subsequent pages. 399 * The provider can keep returning pagination tokens in the returned cursor 400 * by using {@link Cursor#setExtras} until the last page at which point it should not 401 * set a token in the returned cursor. 402 * 403 * @param mediaCategoryId the ID of the media category to filter media sets. 404 * @param extras containing keys to filter media sets: 405 * <ul> 406 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 407 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 408 * <li> {@link android.content.Intent#EXTRA_MIME_TYPES} 409 * </ul> 410 * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. 411 * @return cursor representing {@link CloudMediaProviderContract.MediaSetColumns} columns 412 */ 413 @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) 414 @NonNull onQueryMediaSets(@onNull String mediaCategoryId, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)415 public Cursor onQueryMediaSets(@NonNull String mediaCategoryId, 416 @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { 417 throw new UnsupportedOperationException("onQueryMediaSets is not supported"); 418 } 419 420 /** 421 * Queries for the available SearchSuggestions based on a {@code prefixText}, 422 * filtered by {@code extras}. The columns of SearchSuggestions are in the class 423 * {@link CloudMediaProviderContract.SearchSuggestionColumns} 424 * 425 * <p> 426 * If the user has not started typing, this is considered as zero state suggestion. 427 * In this case {@code prefixText} will be empty string. 428 * 429 * <p> 430 * The order in which suggestions are sorted in the cursor 431 * will be retained when displaying results to the user. 432 * 433 * <p> 434 * The cloud media provider must set the 435 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor 436 * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the 437 * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. 438 * 439 * <p> 440 * {@code extras} may contain some key-value pairs which should be used to prepare 441 * the results. 442 * If the provider handled any params in {@code extras}, it must add the key to 443 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using 444 * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is 445 * without the extra being used. 446 * Note: Currently this function does not pass any key-value params in {@code extras}. 447 * 448 * <p> 449 * Results may not be displayed if it takes longer than 300 milliseconds to get a response from 450 * the cloud media provider. 451 * 452 * @param prefixText the prefix text to filter search suggestions. 453 * @param extras containing keys to filter search suggestions. 454 * <ul> 455 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 456 * </ul> 457 * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. 458 * @return cursor representing search suggestions containing all 459 * {@see CloudMediaProviderContract.SearchSuggestionColumns} columns 460 */ 461 @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) 462 @NonNull onQuerySearchSuggestions(@onNull String prefixText, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)463 public Cursor onQuerySearchSuggestions(@NonNull String prefixText, 464 @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { 465 throw new UnsupportedOperationException("onQuerySearchSuggestions is not supported"); 466 } 467 468 /** 469 * Queries for the available media items under a given {@code mediaSetId}, 470 * filtered by {@code extras}. The columns of Media are in the class 471 * {@link CloudMediaProviderContract.MediaColumns}. {@code mediaSetId} is the ID given 472 * as part of {@link CloudMediaProviderContract.MediaSetColumns#ID} 473 * 474 * <p> 475 * The order in which media items are sorted in the cursor 476 * will be retained when displaying results to the user. 477 * 478 * <p> 479 * The cloud media provider must set the 480 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned 481 * {@link Cursor} by using {@link Cursor#setExtras}. 482 * Not setting this is an error and invalidates the 483 * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. 484 * 485 * <p> 486 * {@code extras} may contain some key-value pairs which should be used to prepare the results. 487 * If the provider handled any filters in {@code extras}, it must add the key to 488 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using 489 * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is 490 * without the extra being used. 491 * 492 * <p> 493 * If the cloud media provider supports pagination, they can set 494 * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, 495 * as part of the returned cursor by using {@link Cursor#setExtras}. 496 * If a token is set, the OS will pass it as a key-value pair in {@code extras} 497 * when querying for media for subsequent pages. 498 * The provider can keep returning pagination tokens in the returned cursor 499 * by using {@link Cursor#setExtras} until the last page at which point it should not 500 * set a token in the returned cursor. 501 * 502 * @param mediaSetId the ID of the media set to filter media items. 503 * @param extras containing keys to filter media items: 504 * <ul> 505 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 506 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 507 * <li> {@link CloudMediaProviderContract#EXTRA_SORT_ORDER} 508 * <li> {@link android.content.Intent#EXTRA_MIME_TYPES} 509 * </ul> 510 * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. 511 * @return cursor representing {@link CloudMediaProviderContract.MediaColumns} columns 512 */ 513 @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) 514 @NonNull onQueryMediaInMediaSet(@onNull String mediaSetId, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)515 public Cursor onQueryMediaInMediaSet(@NonNull String mediaSetId, 516 @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { 517 throw new UnsupportedOperationException("onQueryMediaInMediaSet is not supported"); 518 } 519 520 /** 521 * Searches media items based on a selected suggestion, managed by {@code extras} and 522 * returns a cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. 523 * 524 * <p> 525 * The cloud media provider must set the 526 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor 527 * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the 528 * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. 529 * <p> 530 * 531 * {@code extras} may contain some key-value pairs which should be used to prepare the results. 532 * If the provider handled any params in {@code extras}, it must add the key to 533 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using 534 * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is 535 * without the extra being used. 536 * 537 * <p> 538 * An example user journey: 539 * <ol> 540 * <li>User enters the search prompt.</li> 541 * <li>Using {@link #onQuerySearchSuggestions}, photo picker display suggestions as the user 542 * keeps typing.</li> 543 * <li>User selects a suggestion, Photo picker calls: 544 * {@code onSearchMedia(suggestedMediaSetId, fallbackSearchText, extras)} 545 * with the {@code suggestedMediaSetId} corresponding to the user chosen suggestion. 546 * {@link CloudMediaProviderContract.SearchSuggestionColumns#MEDIA_SET_ID}</li> 547 * </ol> 548 * 549 * 550 * <p> 551 * If the cloud media provider supports pagination, they can set 552 * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, 553 * as part of the returned cursor by using {@link Cursor#setExtras}. 554 * If a token is set, the OS will pass it as a key value pair in {@code extras} 555 * when querying for search media for subsequent pages. 556 * The provider can keep returning pagination tokens in the returned cursor 557 * by using {@link Cursor#setExtras} until the last page at which point it should not 558 * set a token in the returned cursor 559 * 560 * <p> 561 * Results may not be displayed if it takes longer than 3 seconds to get a paged response from 562 * the cloud media provider. 563 * 564 * @param suggestedMediaSetId the media set ID of the suggestion that the user wants to search. 565 * @param fallbackSearchText optional search text to be used when {@code suggestedMediaSetId} 566 * is not useful. 567 * @param extras containing keys to manage the search results: 568 * <ul> 569 * <li>{@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 570 * <li>{@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 571 * <li>{@link CloudMediaProviderContract#EXTRA_SORT_ORDER} 572 * <li> {@link android.content.Intent#EXTRA_MIME_TYPES} 573 * </ul> 574 * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. 575 * @return cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. 576 * @see CloudMediaProviderContract.SearchSuggestionColumns#MEDIA_SET_ID 577 */ 578 @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) 579 @NonNull onSearchMedia(@onNull String suggestedMediaSetId, @Nullable String fallbackSearchText, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)580 public Cursor onSearchMedia(@NonNull String suggestedMediaSetId, 581 @Nullable String fallbackSearchText, 582 @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { 583 throw new UnsupportedOperationException("onSearchMedia for" 584 + " suggestion media set id is not supported"); 585 } 586 587 /** 588 * Searches media items based on entered search text, managed by {@code extras} and 589 * returns a cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. 590 * 591 * <p> 592 * The cloud media provider must set the 593 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned cursor 594 * by using {@link Cursor#setExtras} . Not setting this is an error and invalidates the 595 * returned {@link Cursor}, meaning photo picker will not use the cursor for any operation. 596 * 597 * <p> 598 * {@code extras} may contain some key-value pairs which should be used to prepare the results. 599 * If the provider handled any params in {@code extras}, it must add the key to 600 * the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned cursor by using 601 * {@link Cursor#setExtras}. If not honored, photo picker will assume the result of the query is 602 * without the extra being used. 603 * 604 * <p> 605 * An example user journey: 606 * <ol> 607 * <li>User enters the search prompt.</li> 608 * <li>Using {@link #onQuerySearchSuggestions}, photo picker display suggestions as the user 609 * keeps typing.</li> 610 * <li>User types completely and then enters search, 611 * Photo picker calls: {@code onSearchMedia(searchText, extras)}</li> 612 * </ol> 613 * 614 * <p> 615 * If the cloud media provider supports pagination, they can set 616 * {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} as the next page token, 617 * as part of the returned cursor by using {@link Cursor#setExtras}. 618 * If a token is set, the OS will pass it as a key value pair in {@code extras} 619 * when querying for search media for subsequent pages. 620 * The provider can keep returning pagination tokens in the returned cursor 621 * by using {@link Cursor#setExtras} until the last page at which point it should not 622 * set a token in the returned cursor. 623 * 624 * <p> 625 * Results may not be displayed if it takes longer than 3 seconds to get a paged response from 626 * the cloud media provider. 627 * 628 * @param searchText search text to be used. 629 * @param extras containing keys to manage the search results: 630 * <ul> 631 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN} 632 * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} 633 * <li> {@link CloudMediaProviderContract#EXTRA_SORT_ORDER} 634 * <li> {@link android.content.Intent#EXTRA_MIME_TYPES} 635 * </ul> 636 * @param cancellationSignal {@link CancellationSignal} to check if request has been cancelled. 637 * @return cursor of {@link CloudMediaProviderContract.MediaColumns} based on the match. 638 */ 639 @FlaggedApi(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH) 640 @NonNull onSearchMedia(@onNull String searchText, @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal)641 public Cursor onSearchMedia(@NonNull String searchText, 642 @NonNull Bundle extras, @Nullable CancellationSignal cancellationSignal) { 643 throw new UnsupportedOperationException("onSearchMedia for search text is not supported"); 644 } 645 646 /** 647 * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId} 648 * <p>The cloud media provider should strictly return thumbnail in the original 649 * {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item. 650 * <p> 651 * This is expected to be a much lower resolution version than the item returned by 652 * {@link #onOpenMedia}. 653 * <p> 654 * If you block while downloading content, you should periodically check 655 * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. 656 * 657 * @param mediaId the media item to return 658 * @param size the dimensions of the thumbnail to return. The returned file descriptor doesn't 659 * have to match the {@code size} precisely because the OS will adjust the dimensions before 660 * usage. Implementations can return close approximations especially if the approximation is 661 * already locally on the device and doesn't require downloading from the cloud. 662 * @param extras to modify the way the fd is opened, e.g. for video files we may request a 663 * thumbnail image instead of a video with 664 * {@link CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL} 665 * @param signal used by the OS to signal if the request should be cancelled 666 * @return read-only file descriptor for accessing the thumbnail for the media file 667 * 668 * @see #onOpenMedia 669 * @see CloudMediaProviderContract#EXTRA_PREVIEW_THUMBNAIL 670 */ 671 @SuppressWarnings("unused") 672 @NonNull onOpenPreview(@onNull String mediaId, @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal)673 public abstract AssetFileDescriptor onOpenPreview(@NonNull String mediaId, 674 @NonNull Point size, @Nullable Bundle extras, @Nullable CancellationSignal signal) 675 throws FileNotFoundException; 676 677 /** 678 * Returns the full size media item identified by {@code mediaId}. 679 * <p> 680 * If you block while downloading content, you should periodically check 681 * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. 682 * 683 * @param mediaId the media item to return 684 * @param extras to modify the way the fd is opened, there's none at the moment, but some 685 * might be implemented in the future 686 * @param signal used by the OS to signal if the request should be cancelled 687 * @return read-only file descriptor for accessing the media file 688 * 689 * @see #onOpenPreview 690 */ 691 @SuppressWarnings("unused") 692 @NonNull onOpenMedia(@onNull String mediaId, @Nullable Bundle extras, @Nullable CancellationSignal signal)693 public abstract ParcelFileDescriptor onOpenMedia(@NonNull String mediaId, 694 @Nullable Bundle extras, @Nullable CancellationSignal signal) 695 throws FileNotFoundException; 696 697 /** 698 * Returns a {@link CloudMediaSurfaceController} used for rendering the preview of media items, 699 * or null if preview rendering is not supported. 700 * 701 * @param config containing configuration parameters for {@link CloudMediaSurfaceController} 702 * <ul> 703 * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED} 704 * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED} 705 * </ul> 706 * @param callback {@link CloudMediaSurfaceStateChangedCallback} to send state updates for 707 * {@link Surface} to picker launched via {@link MediaStore#ACTION_PICK_IMAGES} 708 */ 709 @Nullable onCreateCloudMediaSurfaceController(@onNull Bundle config, @NonNull CloudMediaSurfaceStateChangedCallback callback)710 public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config, 711 @NonNull CloudMediaSurfaceStateChangedCallback callback) { 712 return null; 713 } 714 715 /** 716 * Implementation is provided by the parent class. Cannot be overridden. 717 */ 718 @Override 719 @NonNull call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)720 public final Bundle call(@NonNull String method, @Nullable String arg, 721 @Nullable Bundle extras) { 722 if (!method.startsWith("android:")) { 723 // Ignore non-platform methods 724 return super.call(method, arg, extras); 725 } 726 727 try { 728 return callUnchecked(method, arg, extras); 729 } catch (FileNotFoundException e) { 730 throw new RuntimeException(e); 731 } 732 } 733 callUnchecked(@onNull String method, @Nullable String arg, @Nullable Bundle extras)734 private Bundle callUnchecked(@NonNull String method, @Nullable String arg, 735 @Nullable Bundle extras) 736 throws FileNotFoundException { 737 if (extras == null) { 738 extras = new Bundle(); 739 } 740 Bundle result = new Bundle(); 741 if (METHOD_GET_MEDIA_COLLECTION_INFO.equals(method)) { 742 long startTime = System.currentTimeMillis(); 743 result = onGetMediaCollectionInfo(extras); 744 CmpApiVerifier.verifyApiResult(new CmpApiResult( 745 CmpApiVerifier.CloudMediaProviderApis.OnGetMediaCollectionInfo, result), 746 System.currentTimeMillis() - startTime, mAuthority); 747 } else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) { 748 result = onCreateCloudMediaSurfaceController(extras); 749 } else if (METHOD_GET_ASYNC_CONTENT_PROVIDER.equals(method)) { 750 result = onGetAsyncContentProvider(); 751 } else if (Flags.enableCloudMediaProviderCapabilities() 752 && METHOD_GET_CAPABILITIES.equals(method)) { 753 long startTime = System.currentTimeMillis(); 754 755 CloudMediaProviderContract.Capabilities capabilities = onGetCapabilities(); 756 result.putParcelable(EXTRA_PROVIDER_CAPABILITIES, capabilities); 757 758 CmpApiVerifier.verifyApiResult(new CmpApiResult( 759 CmpApiVerifier.CloudMediaProviderApis.OnGetCapabilities, result), 760 System.currentTimeMillis() - startTime, mAuthority); 761 } else { 762 throw new UnsupportedOperationException("Method not supported " + method); 763 } 764 return result; 765 } 766 onCreateCloudMediaSurfaceController(@onNull Bundle extras)767 private Bundle onCreateCloudMediaSurfaceController(@NonNull Bundle extras) { 768 Objects.requireNonNull(extras); 769 770 final IBinder binder = extras.getBinder(EXTRA_SURFACE_STATE_CALLBACK); 771 if (binder == null) { 772 throw new IllegalArgumentException("Missing surface state callback"); 773 } 774 775 final boolean enableLoop = extras.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, 776 DEFAULT_LOOPING_PLAYBACK_ENABLED); 777 final boolean muteAudio = extras.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, 778 DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED); 779 final String authority = extras.getString(EXTRA_AUTHORITY); 780 final CloudMediaSurfaceStateChangedCallback callback = 781 new CloudMediaSurfaceStateChangedCallback( 782 ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder)); 783 final Bundle config = new Bundle(); 784 config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, enableLoop); 785 config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, muteAudio); 786 config.putString(EXTRA_AUTHORITY, authority); 787 final CloudMediaSurfaceController controller = 788 onCreateCloudMediaSurfaceController(config, callback); 789 if (controller == null) { 790 Log.d(TAG, "onCreateCloudMediaSurfaceController returned null"); 791 return Bundle.EMPTY; 792 } 793 794 Bundle result = new Bundle(); 795 result.putBinder(EXTRA_SURFACE_CONTROLLER, 796 new CloudMediaSurfaceControllerWrapper(controller).asBinder()); 797 return result; 798 } 799 800 /** 801 * Implementation is provided by the parent class. Cannot be overridden. 802 * 803 * @see #onOpenMedia 804 */ 805 @NonNull 806 @Override openFile(@onNull Uri uri, @NonNull String mode)807 public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) 808 throws FileNotFoundException { 809 return openFile(uri, mode, null); 810 } 811 812 /** 813 * Implementation is provided by the parent class. Cannot be overridden. 814 * 815 * @see #onOpenMedia 816 */ 817 @NonNull 818 @Override openFile(@onNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal)819 public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode, 820 @Nullable CancellationSignal signal) throws FileNotFoundException { 821 String mediaId = uri.getLastPathSegment(); 822 823 long startTime = System.currentTimeMillis(); 824 ParcelFileDescriptor result = onOpenMedia(mediaId, /* extras */ null, signal); 825 CmpApiVerifier.verifyApiResult(new CmpApiResult( 826 CmpApiVerifier.CloudMediaProviderApis.OnOpenMedia, result), 827 System.currentTimeMillis() - startTime, mAuthority); 828 return result; 829 } 830 831 /** 832 * Implementation is provided by the parent class. Cannot be overridden. 833 * 834 * @see #onOpenPreview 835 * @see #onOpenMedia 836 */ 837 @NonNull 838 @Override openTypedAssetFile(@onNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts)839 public final AssetFileDescriptor openTypedAssetFile(@NonNull Uri uri, 840 @NonNull String mimeTypeFilter, @Nullable Bundle opts) throws FileNotFoundException { 841 return openTypedAssetFile(uri, mimeTypeFilter, opts, null); 842 } 843 844 /** 845 * Implementation is provided by the parent class. Cannot be overridden. 846 * 847 * @see #onOpenPreview 848 * @see #onOpenMedia 849 */ 850 @NonNull 851 @Override openTypedAssetFile( @onNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts, @Nullable CancellationSignal signal)852 public final AssetFileDescriptor openTypedAssetFile( 853 @NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts, 854 @Nullable CancellationSignal signal) throws FileNotFoundException { 855 final String mediaId = uri.getLastPathSegment(); 856 final Bundle bundle = new Bundle(); 857 Point previewSize = null; 858 859 final DisplayMetrics screenMetrics = getContext().getResources().getDisplayMetrics(); 860 int minPreviewLength = Math.min(screenMetrics.widthPixels, screenMetrics.heightPixels); 861 862 if (opts != null) { 863 bundle.putBoolean(EXTRA_MEDIASTORE_THUMB, opts.getBoolean(EXTRA_MEDIASTORE_THUMB)); 864 865 if (opts.containsKey(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)) { 866 bundle.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true); 867 minPreviewLength = minPreviewLength / 2; 868 } 869 870 previewSize = opts.getParcelable(ContentResolver.EXTRA_SIZE); 871 } 872 873 if (previewSize == null) { 874 previewSize = new Point(minPreviewLength, minPreviewLength); 875 } 876 877 long startTime = System.currentTimeMillis(); 878 AssetFileDescriptor result = onOpenPreview(mediaId, previewSize, bundle, signal); 879 CmpApiVerifier.verifyApiResult(new CmpApiResult( 880 CmpApiVerifier.CloudMediaProviderApis.OnOpenPreview, result, previewSize), 881 System.currentTimeMillis() - startTime, mAuthority); 882 return result; 883 } 884 885 /** 886 * Implementation is provided by the parent class. Cannot be overridden. 887 * 888 * @see #onQueryMedia 889 * @see #onQueryDeletedMedia 890 * @see #onQueryAlbums 891 */ 892 @NonNull 893 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)894 public final Cursor query(@NonNull Uri uri, @Nullable String[] projection, 895 @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) { 896 if (queryArgs == null) { 897 queryArgs = new Bundle(); 898 } 899 Cursor result; 900 long startTime = System.currentTimeMillis(); 901 switch (mMatcher.match(uri)) { 902 case MATCH_MEDIAS: 903 result = onQueryMedia(queryArgs); 904 CmpApiVerifier.verifyApiResult(new CmpApiResult( 905 CmpApiVerifier.CloudMediaProviderApis.OnQueryMedia, result), 906 System.currentTimeMillis() - startTime, mAuthority); 907 break; 908 case MATCH_DELETED_MEDIAS: 909 result = onQueryDeletedMedia(queryArgs); 910 CmpApiVerifier.verifyApiResult(new CmpApiResult( 911 CmpApiVerifier.CloudMediaProviderApis.OnQueryDeletedMedia, result), 912 System.currentTimeMillis() - startTime, mAuthority); 913 break; 914 case MATCH_ALBUMS: 915 result = onQueryAlbums(queryArgs); 916 CmpApiVerifier.verifyApiResult(new CmpApiResult( 917 CmpApiVerifier.CloudMediaProviderApis.OnQueryAlbums, result), 918 System.currentTimeMillis() - startTime, mAuthority); 919 break; 920 case MATCH_MEDIA_CATEGORIES: 921 final String parentCategoryId = queryArgs.getString(KEY_PARENT_CATEGORY_ID); 922 queryArgs.remove(KEY_PARENT_CATEGORY_ID); 923 result = onQueryMediaCategories(parentCategoryId, queryArgs, cancellationSignal 924 ); 925 CmpApiVerifier.verifyApiResult(new CmpApiResult( 926 CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaCategories, 927 result), 928 System.currentTimeMillis() - startTime, mAuthority); 929 break; 930 case MATCH_MEDIA_SETS: 931 final String mediaCategoryId = queryArgs.getString(KEY_MEDIA_CATEGORY_ID); 932 queryArgs.remove(KEY_MEDIA_CATEGORY_ID); 933 result = onQueryMediaSets(mediaCategoryId, queryArgs, cancellationSignal); 934 CmpApiVerifier.verifyApiResult(new CmpApiResult( 935 CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaSets, 936 result), 937 System.currentTimeMillis() - startTime, mAuthority); 938 break; 939 case MATCH_SEARCH_SUGGESTION: 940 final String prefixText = queryArgs.getString(KEY_PREFIX_TEXT); 941 queryArgs.remove(KEY_PREFIX_TEXT); 942 result = onQuerySearchSuggestions(prefixText, queryArgs, cancellationSignal); 943 CmpApiVerifier.verifyApiResult(new CmpApiResult( 944 CmpApiVerifier.CloudMediaProviderApis.OnQuerySearchSuggestions, 945 result), 946 System.currentTimeMillis() - startTime, mAuthority); 947 break; 948 case MATCH_MEDIAS_IN_MEDIA_SET: 949 final String mediaSetId = queryArgs.getString(KEY_MEDIA_SET_ID); 950 queryArgs.remove(KEY_MEDIA_SET_ID); 951 result = onQueryMediaInMediaSet(mediaSetId, queryArgs, cancellationSignal); 952 CmpApiVerifier.verifyApiResult(new CmpApiResult( 953 CmpApiVerifier.CloudMediaProviderApis.OnQueryMediaInMediaSet, 954 result), 955 System.currentTimeMillis() - startTime, mAuthority); 956 break; 957 case MATCH_SEARCH: 958 final String searchText = queryArgs.getString(KEY_SEARCH_TEXT); 959 queryArgs.remove(KEY_SEARCH_TEXT); 960 final String searchMediaSetId = queryArgs.getString(KEY_MEDIA_SET_ID); 961 queryArgs.remove(KEY_MEDIA_SET_ID); 962 if (searchMediaSetId != null) { 963 result = onSearchMedia( 964 searchMediaSetId, searchText, queryArgs, cancellationSignal); 965 } else if (searchText != null) { 966 result = onSearchMedia(searchText, queryArgs, cancellationSignal); 967 } else { 968 throw new IllegalArgumentException("both suggested media set id " 969 + "and search text can not be null together"); 970 } 971 CmpApiVerifier.verifyApiResult(new CmpApiResult( 972 CmpApiVerifier.CloudMediaProviderApis.OnSearchMedia, 973 result), 974 System.currentTimeMillis() - startTime, mAuthority); 975 break; 976 default: 977 throw new UnsupportedOperationException("Unsupported Uri " + uri); 978 } 979 return result; 980 } 981 982 /** 983 * Implementation is provided by the parent class. Throws by default, and 984 * cannot be overridden. 985 */ 986 @NonNull 987 @Override getType(@onNull Uri uri)988 public final String getType(@NonNull Uri uri) { 989 throw new UnsupportedOperationException("getType not supported"); 990 } 991 992 /** 993 * Implementation is provided by the parent class. Throws by default, and 994 * cannot be overridden. 995 */ 996 @NonNull 997 @Override canonicalize(@onNull Uri uri)998 public final Uri canonicalize(@NonNull Uri uri) { 999 throw new UnsupportedOperationException("Canonicalize not supported"); 1000 } 1001 1002 /** 1003 * Implementation is provided by the parent class. Throws by default, and 1004 * cannot be overridden. 1005 */ 1006 @NonNull 1007 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)1008 public final Cursor query(@NonNull Uri uri, @Nullable String[] projection, 1009 @Nullable String selection, @Nullable String[] selectionArgs, 1010 @Nullable String sortOrder) { 1011 // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary 1012 // transport method. We override that, and don't ever delegate to this method. 1013 throw new UnsupportedOperationException("Pre-Android-O query format not supported."); 1014 } 1015 1016 /** 1017 * Implementation is provided by the parent class. Throws by default, and 1018 * cannot be overridden. 1019 */ 1020 @NonNull 1021 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal)1022 public final Cursor query(@NonNull Uri uri, @Nullable String[] projection, 1023 @Nullable String selection, @Nullable String[] selectionArgs, 1024 @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) { 1025 // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary 1026 // transport method. We override that, and don't ever delegate to this metohd. 1027 throw new UnsupportedOperationException("Pre-Android-O query format not supported."); 1028 } 1029 1030 /** 1031 * Implementation is provided by the parent class. Throws by default, and 1032 * cannot be overridden. 1033 */ 1034 @NonNull 1035 @Override insert(@onNull Uri uri, @NonNull ContentValues values)1036 public final Uri insert(@NonNull Uri uri, @NonNull ContentValues values) { 1037 throw new UnsupportedOperationException("Insert not supported"); 1038 } 1039 1040 /** 1041 * Implementation is provided by the parent class. Throws by default, and 1042 * cannot be overridden. 1043 */ 1044 @Override delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)1045 public final int delete(@NonNull Uri uri, @Nullable String selection, 1046 @Nullable String[] selectionArgs) { 1047 throw new UnsupportedOperationException("Delete not supported"); 1048 } 1049 1050 /** 1051 * Implementation is provided by the parent class. Throws by default, and 1052 * cannot be overridden. 1053 */ 1054 @Override update(@onNull Uri uri, @NonNull ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)1055 public final int update(@NonNull Uri uri, @NonNull ContentValues values, 1056 @Nullable String selection, @Nullable String[] selectionArgs) { 1057 throw new UnsupportedOperationException("Update not supported"); 1058 } 1059 1060 /** 1061 * Manages rendering the preview of media items on given instances of {@link Surface}. 1062 * 1063 * <p>The methods of this class are meant to be asynchronous, and should not block by performing 1064 * any heavy operation. 1065 * <p>Note that a single CloudMediaSurfaceController instance would be responsible for 1066 * rendering multiple media items associated with multiple surfaces. 1067 */ 1068 @SuppressLint("PackageLayering") // We need to pass in a Surface which can be prepared for 1069 // rendering a media item. 1070 public static abstract class CloudMediaSurfaceController { 1071 1072 /** 1073 * Creates any player resource(s) needed for rendering. 1074 */ onPlayerCreate()1075 public abstract void onPlayerCreate(); 1076 1077 /** 1078 * Releases any player resource(s) used for rendering. 1079 */ onPlayerRelease()1080 public abstract void onPlayerRelease(); 1081 1082 /** 1083 * Indicates creation of the given {@link Surface} with given {@code surfaceId} for 1084 * rendering the preview of a media item with given {@code mediaId}. 1085 * 1086 * <p>This is called immediately after the surface is first created. Implementations of this 1087 * should start up whatever rendering code they desire. 1088 * <p>Note that the given media item remains associated with the given surface id till the 1089 * {@link Surface} is destroyed. 1090 * 1091 * @param surfaceId id which uniquely identifies the {@link Surface} for rendering 1092 * @param surface instance of the {@link Surface} on which the media item should be rendered 1093 * @param mediaId id which uniquely identifies the media to be rendered 1094 * 1095 * @see SurfaceHolder.Callback#surfaceCreated(SurfaceHolder) 1096 */ onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)1097 public abstract void onSurfaceCreated(int surfaceId, @NonNull Surface surface, 1098 @NonNull String mediaId); 1099 1100 /** 1101 * Indicates structural changes (format or size) in the {@link Surface} for rendering. 1102 * 1103 * <p>This method is always called at least once, after {@link #onSurfaceCreated}. 1104 * 1105 * @param surfaceId id which uniquely identifies the {@link Surface} for rendering 1106 * @param format the new {@link PixelFormat} of the surface 1107 * @param width the new width of the {@link Surface} 1108 * @param height the new height of the {@link Surface} 1109 * 1110 * @see SurfaceHolder.Callback#surfaceChanged(SurfaceHolder, int, int, int) 1111 */ onSurfaceChanged(int surfaceId, int format, int width, int height)1112 public abstract void onSurfaceChanged(int surfaceId, int format, int width, int height); 1113 1114 /** 1115 * Indicates destruction of a {@link Surface} with given {@code surfaceId}. 1116 * 1117 * <p>This is called immediately before a surface is being destroyed. After returning from 1118 * this call, you should no longer try to access this surface. 1119 * 1120 * @param surfaceId id which uniquely identifies the {@link Surface} for rendering 1121 * 1122 * @see SurfaceHolder.Callback#surfaceDestroyed(SurfaceHolder) 1123 */ onSurfaceDestroyed(int surfaceId)1124 public abstract void onSurfaceDestroyed(int surfaceId); 1125 1126 /** 1127 * Start playing the preview of the media associated with the given surface id. If 1128 * playback had previously been paused, playback will continue from where it was paused. 1129 * If playback had been stopped, or never started before, playback will start at the 1130 * beginning. 1131 * 1132 * @param surfaceId id which uniquely identifies the {@link Surface} for rendering 1133 */ onMediaPlay(int surfaceId)1134 public abstract void onMediaPlay(int surfaceId); 1135 1136 /** 1137 * Pauses the playback of the media associated with the given surface id. 1138 * 1139 * @param surfaceId id which uniquely identifies the {@link Surface} for rendering 1140 */ onMediaPause(int surfaceId)1141 public abstract void onMediaPause(int surfaceId); 1142 1143 /** 1144 * Seeks the media associated with the given surface id to specified timestamp. 1145 * 1146 * @param surfaceId id which uniquely identifies the {@link Surface} for rendering 1147 * @param timestampMillis the timestamp in milliseconds from the start to seek to 1148 */ onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)1149 public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis); 1150 1151 /** 1152 * Changes the configuration parameters for the CloudMediaSurfaceController. 1153 * 1154 * @param config the updated config to change to. This can include config changes for the 1155 * following: 1156 * <ul> 1157 * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED} 1158 * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED} 1159 * </ul> 1160 */ onConfigChange(@onNull Bundle config)1161 public abstract void onConfigChange(@NonNull Bundle config); 1162 1163 /** 1164 * Indicates destruction of this CloudMediaSurfaceController object. 1165 * 1166 * <p>This CloudMediaSurfaceController object should no longer be in use after this method 1167 * has been called. 1168 * 1169 * <p>Note that it is possible for this method to be called directly without 1170 * {@link #onPlayerRelease} being called, hence you should release any resources associated 1171 * with this CloudMediaSurfaceController object, or perform any cleanup required in this 1172 * method. 1173 */ onDestroy()1174 public abstract void onDestroy(); 1175 } 1176 1177 /** 1178 * This class is used by {@link CloudMediaProvider} to send {@link Surface} state updates to 1179 * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}. 1180 * 1181 * @see MediaStore#ACTION_PICK_IMAGES 1182 */ 1183 public static final class CloudMediaSurfaceStateChangedCallback { 1184 1185 /** {@hide} */ 1186 @IntDef(flag = true, prefix = { "PLAYBACK_STATE_" }, value = { 1187 PLAYBACK_STATE_BUFFERING, 1188 PLAYBACK_STATE_READY, 1189 PLAYBACK_STATE_STARTED, 1190 PLAYBACK_STATE_PAUSED, 1191 PLAYBACK_STATE_COMPLETED, 1192 PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE, 1193 PLAYBACK_STATE_ERROR_PERMANENT_FAILURE, 1194 PLAYBACK_STATE_MEDIA_SIZE_CHANGED 1195 }) 1196 @Retention(RetentionPolicy.SOURCE) 1197 public @interface PlaybackState {} 1198 1199 /** 1200 * Constant to notify that the playback is buffering 1201 */ 1202 public static final int PLAYBACK_STATE_BUFFERING = 1; 1203 1204 /** 1205 * Constant to notify that the playback is ready to be played 1206 */ 1207 public static final int PLAYBACK_STATE_READY = 2; 1208 1209 /** 1210 * Constant to notify that the playback has started 1211 */ 1212 public static final int PLAYBACK_STATE_STARTED = 3; 1213 1214 /** 1215 * Constant to notify that the playback is paused. 1216 */ 1217 public static final int PLAYBACK_STATE_PAUSED = 4; 1218 1219 /** 1220 * Constant to notify that the playback has completed 1221 */ 1222 public static final int PLAYBACK_STATE_COMPLETED = 5; 1223 1224 /** 1225 * Constant to notify that the playback has failed with a retriable error. 1226 */ 1227 public static final int PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE = 6; 1228 1229 /** 1230 * Constant to notify that the playback has failed with a permanent error. 1231 */ 1232 public static final int PLAYBACK_STATE_ERROR_PERMANENT_FAILURE = 7; 1233 1234 /** 1235 * Constant to notify that the media size is first known or has changed. 1236 * 1237 * Pass the width and height of the media as a {@link Point} inside the {@link Bundle} with 1238 * {@link ContentResolver#EXTRA_SIZE} as the key. 1239 * 1240 * @see CloudMediaSurfaceStateChangedCallback#setPlaybackState(int, int, Bundle) 1241 * @see MediaPlayer.OnVideoSizeChangedListener#onVideoSizeChanged(MediaPlayer, int, int) 1242 */ 1243 public static final int PLAYBACK_STATE_MEDIA_SIZE_CHANGED = 8; 1244 1245 private final ICloudMediaSurfaceStateChangedCallback mCallback; 1246 CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback)1247 CloudMediaSurfaceStateChangedCallback(ICloudMediaSurfaceStateChangedCallback callback) { 1248 mCallback = callback; 1249 } 1250 1251 /** 1252 * This is called to notify playback state update for a {@link Surface} 1253 * on the picker launched via {@link MediaStore#ACTION_PICK_IMAGES}. 1254 * 1255 * @param surfaceId id which uniquely identifies a {@link Surface} 1256 * @param playbackState playback state to notify picker about 1257 * @param playbackStateInfo {@link Bundle} which may contain extra information about the 1258 * playback state, such as media size, progress/seek info or 1259 * details about errors. 1260 */ setPlaybackState(int surfaceId, @PlaybackState int playbackState, @Nullable Bundle playbackStateInfo)1261 public void setPlaybackState(int surfaceId, @PlaybackState int playbackState, 1262 @Nullable Bundle playbackStateInfo) { 1263 try { 1264 mCallback.setPlaybackState(surfaceId, playbackState, playbackStateInfo); 1265 } catch (Exception e) { 1266 Log.w(TAG, "Failed to notify playback state (" + playbackState + ") for " 1267 + "surfaceId: " + surfaceId + " ; playbackStateInfo: " + playbackStateInfo, 1268 e); 1269 } 1270 } 1271 1272 /** 1273 * Returns the underliying {@link IBinder} object. 1274 * 1275 * @hide 1276 */ getIBinder()1277 public IBinder getIBinder() { 1278 return mCallback.asBinder(); 1279 } 1280 } 1281 1282 /** 1283 * {@link Binder} object backing a {@link CloudMediaSurfaceController} instance. 1284 * 1285 * @hide 1286 */ 1287 public static class CloudMediaSurfaceControllerWrapper 1288 extends ICloudMediaSurfaceController.Stub { 1289 1290 final private CloudMediaSurfaceController mSurfaceController; 1291 CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController)1292 CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController) { 1293 mSurfaceController = surfaceController; 1294 } 1295 1296 @Override onPlayerCreate()1297 public void onPlayerCreate() { 1298 Log.i(TAG, "Creating player."); 1299 mSurfaceController.onPlayerCreate(); 1300 } 1301 1302 @Override onPlayerRelease()1303 public void onPlayerRelease() { 1304 Log.i(TAG, "Releasing player."); 1305 mSurfaceController.onPlayerRelease(); 1306 } 1307 1308 @Override onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)1309 public void onSurfaceCreated(int surfaceId, @NonNull Surface surface, 1310 @NonNull String mediaId) { 1311 Log.i(TAG, "Surface prepared. SurfaceId: " + surfaceId + ". MediaId: " + mediaId); 1312 mSurfaceController.onSurfaceCreated(surfaceId, surface, mediaId); 1313 } 1314 1315 @Override onSurfaceChanged(int surfaceId, int format, int width, int height)1316 public void onSurfaceChanged(int surfaceId, int format, int width, int height) { 1317 Log.i(TAG, "Surface changed. SurfaceId: " + surfaceId + ". Format: " + format 1318 + ". Width: " + width + ". Height: " + height); 1319 mSurfaceController.onSurfaceChanged(surfaceId, format, width, height); 1320 } 1321 1322 @Override onSurfaceDestroyed(int surfaceId)1323 public void onSurfaceDestroyed(int surfaceId) { 1324 Log.i(TAG, "Surface released. SurfaceId: " + surfaceId); 1325 mSurfaceController.onSurfaceDestroyed(surfaceId); 1326 } 1327 1328 @Override onMediaPlay(int surfaceId)1329 public void onMediaPlay(int surfaceId) { 1330 Log.i(TAG, "Media played. SurfaceId: " + surfaceId); 1331 mSurfaceController.onMediaPlay(surfaceId); 1332 } 1333 1334 @Override onMediaPause(int surfaceId)1335 public void onMediaPause(int surfaceId) { 1336 Log.i(TAG, "Media paused. SurfaceId: " + surfaceId); 1337 mSurfaceController.onMediaPause(surfaceId); 1338 } 1339 1340 @Override onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)1341 public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) { 1342 Log.i(TAG, "Media seeked. SurfaceId: " + surfaceId + ". Seek timestamp(ms): " 1343 + timestampMillis); 1344 mSurfaceController.onMediaSeekTo(surfaceId, timestampMillis); 1345 } 1346 1347 @Override onConfigChange(@onNull Bundle config)1348 public void onConfigChange(@NonNull Bundle config) { 1349 Log.i(TAG, "Config changed. Updated config params: " + config); 1350 mSurfaceController.onConfigChange(config); 1351 } 1352 1353 @Override onDestroy()1354 public void onDestroy() { 1355 Log.i(TAG, "Controller destroyed"); 1356 mSurfaceController.onDestroy(); 1357 } 1358 } 1359 1360 /** 1361 * @hide 1362 */ 1363 private class AsyncContentProviderWrapper extends IAsyncContentProvider.Stub { 1364 1365 @Override openMedia(String mediaId, RemoteCallback remoteCallback)1366 public void openMedia(String mediaId, RemoteCallback remoteCallback) { 1367 try { 1368 ParcelFileDescriptor pfd = onOpenMedia(mediaId,/* extras */ 1369 null,/* cancellationSignal */ null); 1370 sendResult(pfd, null, remoteCallback); 1371 } catch (Exception e) { 1372 sendResult(null, e, remoteCallback); 1373 } 1374 } 1375 sendResult(ParcelFileDescriptor pfd, Throwable throwable, RemoteCallback remoteCallback)1376 private void sendResult(ParcelFileDescriptor pfd, Throwable throwable, 1377 RemoteCallback remoteCallback) { 1378 Bundle bundle = new Bundle(); 1379 if (pfd == null && throwable == null) { 1380 throw new IllegalStateException("Expected ParcelFileDescriptor or an exception."); 1381 } 1382 if (pfd != null) { 1383 bundle.putParcelable(EXTRA_FILE_DESCRIPTOR, pfd); 1384 } 1385 if (throwable != null) { 1386 bundle.putString(EXTRA_ERROR_MESSAGE, throwable.getMessage()); 1387 } 1388 remoteCallback.sendResult(bundle); 1389 } 1390 } 1391 } 1392