1 /* 2 * Copyright (C) 2024 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 com.android.providers.media.photopicker.v2; 18 19 import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_ALBUM; 20 21 import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN; 22 import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE; 23 import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN; 24 import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; 25 import static com.android.providers.media.MediaProvider.isOwnedPhotosEnabled; 26 import static com.android.providers.media.PickerUriResolver.getAlbumUri; 27 import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid; 28 import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId; 29 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID; 30 import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_GRANTS_SYNC_WORK_NAME; 31 import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_LOCAL_SYNC_WORK_NAME; 32 import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY; 33 import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY; 34 import static com.android.providers.media.photopicker.sync.WorkManagerInitializer.getWorkManager; 35 import static com.android.providers.media.photopicker.v2.SearchSuggestionsProvider.getDefaultSuggestions; 36 import static com.android.providers.media.photopicker.v2.SearchSuggestionsProvider.getSuggestionsFromCloudProvider; 37 import static com.android.providers.media.photopicker.v2.SearchSuggestionsProvider.getSuggestionsFromLocalProvider; 38 import static com.android.providers.media.photopicker.v2.SearchSuggestionsProvider.maybeCacheSearchSuggestions; 39 import static com.android.providers.media.photopicker.v2.SearchSuggestionsProvider.suggestionsToCursor; 40 import static com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper.EMPTY_MEDIA_ID; 41 import static com.android.providers.media.photopicker.v2.model.MediaGroup.ALBUM; 42 import static com.android.providers.media.photopicker.v2.model.MediaGroup.CATEGORY; 43 44 import static java.util.Objects.requireNonNull; 45 46 import android.annotation.UserIdInt; 47 import android.content.Context; 48 import android.content.Intent; 49 import android.content.pm.PackageManager; 50 import android.content.pm.PackageManager.NameNotFoundException; 51 import android.content.pm.ProviderInfo; 52 import android.database.Cursor; 53 import android.database.MatrixCursor; 54 import android.database.MergeCursor; 55 import android.database.sqlite.SQLiteDatabase; 56 import android.database.sqlite.SQLiteQueryBuilder; 57 import android.os.Bundle; 58 import android.os.CancellationSignal; 59 import android.os.Process; 60 import android.provider.CloudMediaProviderContract; 61 import android.provider.CloudMediaProviderContract.AlbumColumns; 62 import android.provider.MediaStore; 63 import android.util.Log; 64 import android.util.Pair; 65 66 import androidx.annotation.NonNull; 67 import androidx.annotation.Nullable; 68 import androidx.work.WorkManager; 69 70 import com.android.providers.media.flags.Flags; 71 import com.android.providers.media.photopicker.PickerSyncController; 72 import com.android.providers.media.photopicker.SearchState; 73 import com.android.providers.media.photopicker.sync.PickerSearchProviderClient; 74 import com.android.providers.media.photopicker.sync.PickerSyncManager; 75 import com.android.providers.media.photopicker.sync.SyncCompletionWaiter; 76 import com.android.providers.media.photopicker.sync.SyncTrackerRegistry; 77 import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException; 78 import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException; 79 import com.android.providers.media.photopicker.v2.model.AlbumMediaQuery; 80 import com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper; 81 import com.android.providers.media.photopicker.v2.model.MediaGroup; 82 import com.android.providers.media.photopicker.v2.model.MediaInMediaSetSyncRequestParams; 83 import com.android.providers.media.photopicker.v2.model.MediaQuery; 84 import com.android.providers.media.photopicker.v2.model.MediaQueryForPreSelection; 85 import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams; 86 import com.android.providers.media.photopicker.v2.model.MediaSource; 87 import com.android.providers.media.photopicker.v2.model.PreviewMediaQuery; 88 import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo; 89 import com.android.providers.media.photopicker.v2.model.SearchRequest; 90 import com.android.providers.media.photopicker.v2.model.SearchSuggestion; 91 import com.android.providers.media.photopicker.v2.model.SearchSuggestionRequest; 92 import com.android.providers.media.photopicker.v2.sqlite.MediaGroupCursorUtils; 93 import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsDatabaseUtil; 94 import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsQuery; 95 import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil; 96 import com.android.providers.media.photopicker.v2.sqlite.PickerMediaDatabaseUtil; 97 import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants; 98 import com.android.providers.media.photopicker.v2.sqlite.SearchMediaQuery; 99 import com.android.providers.media.photopicker.v2.sqlite.SearchRequestDatabaseUtil; 100 import com.android.providers.media.photopicker.v2.sqlite.SearchResultsDatabaseUtil; 101 import com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsDatabaseUtils; 102 import com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsQuery; 103 104 import java.util.ArrayList; 105 import java.util.Arrays; 106 import java.util.HashMap; 107 import java.util.HashSet; 108 import java.util.List; 109 import java.util.Locale; 110 import java.util.Map; 111 import java.util.Objects; 112 import java.util.Set; 113 import java.util.concurrent.CompletableFuture; 114 import java.util.concurrent.ExecutionException; 115 import java.util.concurrent.Executor; 116 import java.util.concurrent.ForkJoinPool; 117 import java.util.concurrent.TimeUnit; 118 import java.util.concurrent.TimeoutException; 119 120 /** 121 * This class handles Photo Picker content queries. 122 */ 123 public class PickerDataLayerV2 { 124 private static final String TAG = "PickerDataLayerV2"; 125 private static final int CLOUD_SYNC_TIMEOUT_MILLIS = 500; 126 private static final String MEDIA_TABLE = "media"; 127 private static final String MEDIA_LEFT_JOIN_MEDIA_GRANTS_TABLE = 128 MEDIA_TABLE + " LEFT JOIN " + MEDIA_GRANTS_TABLE 129 + " ON " + MEDIA_TABLE + "." + KEY_LOCAL_ID 130 + " = " + MEDIA_GRANTS_TABLE + "." + FILE_ID_COLUMN; 131 132 // Local and merged albums have a predefined order that they should be displayed in. They always 133 // need to be displayed above the cloud albums too. 134 public static final List<String> PINNED_ALBUMS_ORDER = List.of( 135 AlbumColumns.ALBUM_ID_FAVORITES, 136 AlbumColumns.ALBUM_ID_CAMERA, 137 AlbumColumns.ALBUM_ID_VIDEOS, 138 AlbumColumns.ALBUM_ID_SCREENSHOTS, 139 AlbumColumns.ALBUM_ID_DOWNLOADS 140 ); 141 142 // Pinned albums and categories have a predefined order that they should be displayed in. 143 public static final List<Pair<MediaGroup, String>> PINNED_CATEGORIES_AND_ALBUMS_ORDER = List.of( 144 new Pair<>(ALBUM, AlbumColumns.ALBUM_ID_FAVORITES), 145 new Pair<>(ALBUM, AlbumColumns.ALBUM_ID_CAMERA), 146 new Pair<>(CATEGORY, CloudMediaProviderContract.MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS), 147 new Pair<>(ALBUM, AlbumColumns.ALBUM_ID_DOWNLOADS), 148 new Pair<>(ALBUM, AlbumColumns.ALBUM_ID_SCREENSHOTS), 149 new Pair<>(ALBUM, AlbumColumns.ALBUM_ID_VIDEOS) 150 ); 151 152 // Set of known merged albums. 153 public static final Set<String> MERGED_ALBUMS = Set.of( 154 AlbumColumns.ALBUM_ID_FAVORITES, 155 AlbumColumns.ALBUM_ID_VIDEOS 156 ); 157 158 // Set of known local albums. 159 public static final Set<String> LOCAL_ALBUMS = Set.of( 160 AlbumColumns.ALBUM_ID_CAMERA, 161 AlbumColumns.ALBUM_ID_SCREENSHOTS, 162 AlbumColumns.ALBUM_ID_DOWNLOADS 163 ); 164 165 /** 166 * Table used to store the items for which the app hold read grants but have been de-selected 167 * by the user in the current photo-picker session. 168 */ 169 public static final String DE_SELECTIONS_TABLE = "de_selections"; 170 171 /** 172 * Table used to store the items for which the app hold read grants but have been de-selected 173 * by the user in the current photo-picker session, filtered by calling package name and userId. 174 */ 175 public static final String CURRENT_DE_SELECTIONS_TABLE = "current_de_selections"; 176 177 private static final String IS_FIRST_PAGE = "is_first_page"; 178 /** 179 * In SQL joins for media_grants table, it is filtered to only provide the rows corresponding to 180 * the current package and userId. This is the name for the filtered table that is computed in a 181 * sub-query. Any references to the columns for media_grants table should use this table name 182 * instead. 183 */ 184 public static final String CURRENT_GRANTS_TABLE = "current_media_grants"; 185 186 public static final String COLUMN_GRANTS_COUNT = "grants_count"; 187 188 private static final String PROJECTION_GRANTS_COUNT = String.format( 189 Locale.ROOT, "COUNT(*) AS %s", 190 COLUMN_GRANTS_COUNT); 191 192 /** 193 * Refresh the cloud provider in-memory cache in PickerSyncController. 194 */ ensureProviders()195 public static void ensureProviders() { 196 try { 197 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 198 syncController.maybeEnableCloudMediaQueries(); 199 } catch (UnableToAcquireLockException | RequestObsoleteException exception) { 200 Log.e(TAG, "Could not ensure that the providers are set."); 201 } 202 } 203 204 /** 205 * Returns a cursor with the Photo Picker media in response. 206 * 207 * @param appContext The application context. 208 * @param queryArgs The arguments help us filter on the media query to yield the desired 209 * results. 210 */ 211 @NonNull queryMedia(@onNull Context appContext, @NonNull Bundle queryArgs)212 public static Cursor queryMedia(@NonNull Context appContext, @NonNull Bundle queryArgs) { 213 final MediaQuery query = new MediaQuery(queryArgs); 214 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 215 final String effectiveLocalAuthority = 216 query.getProviders().contains(syncController.getLocalProvider()) 217 ? syncController.getLocalProvider() 218 : null; 219 final String cloudAuthority = syncController 220 .getCloudProviderOrDefault(/* defaultValue */ null); 221 final String effectiveCloudAuthority = 222 syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority) 223 ? cloudAuthority 224 : null; 225 226 waitForOngoingSync(appContext, effectiveLocalAuthority, effectiveCloudAuthority, 227 query.getIntentAction()); 228 229 return PickerMediaDatabaseUtil.queryMedia( 230 appContext, 231 syncController, 232 query, 233 effectiveLocalAuthority, 234 effectiveCloudAuthority 235 ); 236 } 237 238 /** 239 * Returns a cursor with the Photo Picker media in response. 240 * 241 * @param appContext The application context. 242 * @param queryArgs The arguments help us filter on the media query to yield the desired 243 * results. 244 */ 245 @NonNull queryPreviewMedia(@onNull Context appContext, @NonNull Bundle queryArgs)246 static Cursor queryPreviewMedia(@NonNull Context appContext, @NonNull Bundle queryArgs) { 247 final PreviewMediaQuery query = new PreviewMediaQuery(queryArgs, appContext); 248 249 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 250 final String effectiveLocalAuthority = 251 query.getProviders().contains(syncController.getLocalProvider()) 252 ? syncController.getLocalProvider() 253 : null; 254 final String cloudAuthority = syncController 255 .getCloudProviderOrDefault(/* defaultValue */ null); 256 final String effectiveCloudAuthority = 257 syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority) 258 ? cloudAuthority 259 : null; 260 261 if (queryArgs.getBoolean(IS_FIRST_PAGE)) { 262 PreviewMediaQuery.insertDeSelections(appContext, syncController, 263 query.getCallingPackageUid(), query.getCurrentDeSelection()); 264 } 265 266 waitForOngoingSync(appContext, effectiveLocalAuthority, effectiveCloudAuthority, 267 query.getIntentAction()); 268 269 return PickerMediaDatabaseUtil.queryMedia( 270 appContext, 271 syncController, 272 query, 273 effectiveLocalAuthority, 274 effectiveCloudAuthority 275 ); 276 } 277 278 /** 279 * Returns a cursor with cached media sets in response 280 * @param queryArgs The arguments to filter and fetch media sets 281 */ 282 @NonNull queryMediaSets(@onNull Bundle queryArgs)283 public static Cursor queryMediaSets(@NonNull Bundle queryArgs) { 284 requireNonNull(queryArgs); 285 286 MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(queryArgs); 287 PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 288 final Set<String> providers = new HashSet<>( 289 Objects.requireNonNull(queryArgs.getStringArrayList("providers"))); 290 final String effectiveLocalAuthority = providers.contains( 291 syncController.getLocalProvider()) ? syncController.getLocalProvider() : null; 292 final String currentCloudAuthority = syncController 293 .getCloudProviderOrDefault(/*defaultValue*/ null); 294 final String effectiveCloudAuthority = syncController 295 .shouldQueryCloudMediaSets(providers, currentCloudAuthority) 296 ? currentCloudAuthority : null; 297 298 waitForOngoingMediaSetsSync(effectiveLocalAuthority, effectiveCloudAuthority); 299 300 Cursor mediaSetsCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory( 301 syncController.getDbFacade().getDatabase(), 302 requestParams 303 ); 304 305 return MediaGroupCursorUtils.getMediaGroupCursorForMediaSets(mediaSetsCursor); 306 } 307 308 /** 309 * Returns a cursor with the Photo Picker albums in response. 310 * 311 * @param appContext The application context. 312 * @param queryArgs The arguments help us filter on the media query to yield the desired 313 * results. 314 */ 315 @Nullable queryAlbums(@onNull Context appContext, @NonNull Bundle queryArgs)316 static Cursor queryAlbums(@NonNull Context appContext, @NonNull Bundle queryArgs) { 317 final MediaQuery query = new MediaQuery(queryArgs); 318 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 319 final SQLiteDatabase database = syncController.getDbFacade().getDatabase(); 320 final String localAuthority = syncController.getLocalProvider(); 321 final boolean shouldShowLocalAlbums = query.getProviders().contains(localAuthority); 322 final String cloudAuthority = 323 syncController.getCloudProviderOrDefault(/* defaultValue */ null); 324 final boolean shouldShowCloudAlbums = syncController.shouldQueryCloudMedia( 325 query.getProviders(), cloudAuthority); 326 final List<AlbumsCursorWrapper> allAlbumCursors = new ArrayList<>(); 327 328 final String effectiveLocalAuthority = shouldShowLocalAlbums ? localAuthority : null; 329 final String effectiveCloudAuthority = shouldShowCloudAlbums ? cloudAuthority : null; 330 331 // Get all local albums from the local provider in separate cursors to facilitate zipping 332 // them with merged albums. 333 final Map<String, AlbumsCursorWrapper> localAlbums = getLocalAlbumCursors( 334 appContext, query, effectiveLocalAuthority); 335 336 // Add Pinned album cursors to the list of all album cursors in the order in which they 337 // should be displayed. Note that pinned albums can only be local and merged albums. 338 for (String albumId: PINNED_ALBUMS_ORDER) { 339 final AlbumsCursorWrapper albumCursor; 340 if (MERGED_ALBUMS.contains(albumId)) { 341 albumCursor = PickerMediaDatabaseUtil.getMergedAlbumsCursor( 342 albumId, appContext, queryArgs, database, 343 effectiveLocalAuthority, effectiveCloudAuthority); 344 } else if (LOCAL_ALBUMS.contains(albumId)) { 345 albumCursor = localAlbums.getOrDefault(albumId, null); 346 } else { 347 Log.e(TAG, "Could not recognize pinned album id, skipping it : " + albumId); 348 albumCursor = null; 349 } 350 allAlbumCursors.add(albumCursor); 351 } 352 353 // Add cloud albums at the end. 354 // This is an external query into the CMP, so catch any exceptions that might get thrown 355 // so that at a minimum, the local results are sent back to the UI. 356 try { 357 allAlbumCursors.add(getCloudAlbumsCursor(appContext, query, localAuthority, 358 effectiveCloudAuthority)); 359 } catch (RuntimeException ex) { 360 Log.w(TAG, "Cloud provider exception while fetching cloud albums cursor", ex); 361 } 362 363 // Remove empty cursors. 364 allAlbumCursors.removeIf(it -> it == null || !it.moveToFirst()); 365 366 if (allAlbumCursors.isEmpty()) { 367 Log.e(TAG, "No albums available"); 368 return null; 369 } else { 370 Cursor mergeCursor = new MergeCursor(allAlbumCursors.toArray(new Cursor[0])); 371 Log.i(TAG, "Returning " + mergeCursor.getCount() + " albums' metadata"); 372 return mergeCursor; 373 } 374 } 375 376 /** 377 * Returns a cursor with the Photo Picker albums and categories in response. 378 * 379 * @param appContext The application context. 380 * @param queryArgs The arguments help us filter on the media query to yield the desired 381 * results. 382 * @param cancellationSignal CancellationSignal object that notifies if the request has been 383 * cancelled. 384 */ 385 @Nullable queryCategoriesAndAlbums( @onNull Context appContext, @NonNull Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)386 public static Cursor queryCategoriesAndAlbums( 387 @NonNull Context appContext, 388 @NonNull Bundle queryArgs, 389 @Nullable CancellationSignal cancellationSignal) { 390 final MediaQuery query = new MediaQuery(queryArgs); 391 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 392 final String localAuthority = syncController.getLocalProvider(); 393 final boolean shouldShowLocalAlbums = query.getProviders().contains(localAuthority); 394 final String cloudAuthority = 395 syncController.getCloudProviderOrDefault(/* defaultValue */ null); 396 final boolean shouldShowCloudAlbums = syncController.shouldQueryCloudMedia( 397 query.getProviders(), cloudAuthority); 398 399 final String effectiveLocalAuthority = shouldShowLocalAlbums ? localAuthority : null; 400 final String effectiveCloudAuthority = shouldShowCloudAlbums ? cloudAuthority : null; 401 402 final SQLiteDatabase database = PickerSyncController.getInstanceOrThrow() 403 .getDbFacade().getDatabase(); 404 final List<Cursor> allMediaGroupCursors = new ArrayList<>(); 405 406 // Get all local albums from the local provider in separate cursors to facilitate zipping 407 // them with merged albums. 408 final Map<String, AlbumsCursorWrapper> localAlbums = getLocalAlbumCursors( 409 appContext, query, effectiveLocalAuthority); 410 411 // Get cloud categories from cloud provider. 412 final Cursor categories = getCloudCategories( 413 appContext, query, effectiveCloudAuthority, syncController, cancellationSignal); 414 415 // Add Pinned album and categories to the list of cursors in the order in which they 416 // should be displayed. Note that pinned albums can only be local and merged albums. 417 long index = 0; 418 for (Pair<MediaGroup, String> mediaGroup: PINNED_CATEGORIES_AND_ALBUMS_ORDER) { 419 final Cursor cursor; 420 switch (mediaGroup.first) { 421 case ALBUM: 422 final String albumId = mediaGroup.second; 423 if (MERGED_ALBUMS.contains(albumId)) { 424 final Cursor mergedAlbumCursor = 425 PickerMediaDatabaseUtil.getMergedAlbumsCursor( 426 albumId, appContext, queryArgs, database, effectiveLocalAuthority, 427 effectiveCloudAuthority); 428 cursor = MediaGroupCursorUtils.getMediaGroupCursorForAlbums( 429 mergedAlbumCursor, index); 430 } else if (LOCAL_ALBUMS.contains(albumId)) { 431 final Cursor localAlbumCursor = localAlbums.getOrDefault(albumId, null); 432 cursor = MediaGroupCursorUtils.getMediaGroupCursorForAlbums( 433 localAlbumCursor, index); 434 } else { 435 Log.e(TAG, "Could not recognize pinned album id, skipping it : " + albumId); 436 cursor = null; 437 } 438 439 break; 440 case CATEGORY: 441 switch (mediaGroup.second) { 442 case CloudMediaProviderContract.MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS: 443 cursor = MediaGroupCursorUtils.getMediaGroupCursorForCategories( 444 categories, effectiveCloudAuthority, index); 445 break; 446 default: 447 Log.e(TAG, "Could not recognize pinned category type, skipping it : " 448 + mediaGroup.second); 449 cursor = null; 450 } 451 452 break; 453 default: 454 Log.e(TAG, "Could not recognize media group, skipping it : " + mediaGroup); 455 cursor = null; 456 } 457 458 if (cursor != null) { 459 index += cursor.getCount(); 460 allMediaGroupCursors.add(cursor); 461 } 462 } 463 464 // Add cloud albums at the end. 465 // This is an external query into the CMP, so catch any exceptions that might get thrown 466 // so that at a minimum, the local results are sent back to the UI. 467 try { 468 final Cursor cloudAlbumsCursor = getCloudAlbumsCursor(appContext, query, 469 localAuthority, effectiveCloudAuthority); 470 allMediaGroupCursors.add( 471 MediaGroupCursorUtils.getMediaGroupCursorForAlbums(cloudAlbumsCursor, index)); 472 } catch (RuntimeException ex) { 473 Log.w(TAG, "Cloud provider exception while fetching cloud albums cursor", ex); 474 } 475 476 // Remove empty cursors. 477 allMediaGroupCursors.removeIf(it -> it == null || !it.moveToFirst()); 478 479 if (allMediaGroupCursors.isEmpty()) { 480 Log.e(TAG, "No categories or albums available"); 481 return null; 482 } else { 483 Cursor mergeCursor = new MergeCursor( 484 allMediaGroupCursors.toArray( 485 new Cursor[allMediaGroupCursors.size()])); 486 Log.i(TAG, "Returning " + mergeCursor.getCount() + " categories and albums."); 487 return mergeCursor; 488 } 489 } 490 491 /** 492 * Returns a cursor with the Photo Picker album media in response. 493 * 494 * @param appContext The application context. 495 * @param queryArgs The arguments help us filter on the media query to yield the desired 496 * results. 497 * @param albumId The album ID of the requested album media. 498 */ queryAlbumMedia( @onNull Context appContext, @NonNull Bundle queryArgs, @NonNull String albumId)499 static Cursor queryAlbumMedia( 500 @NonNull Context appContext, 501 @NonNull Bundle queryArgs, 502 @NonNull String albumId) { 503 final AlbumMediaQuery query = new AlbumMediaQuery(queryArgs, albumId); 504 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 505 final String effectiveLocalAuthority = 506 query.getProviders().contains(syncController.getLocalProvider()) 507 ? syncController.getLocalProvider() 508 : null; 509 final String cloudAuthority = syncController 510 .getCloudProviderOrDefault(/* defaultValue */ null); 511 final String effectiveCloudAuthority = 512 syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority) 513 ? cloudAuthority 514 : null; 515 516 if (MERGED_ALBUMS.contains(albumId)) { 517 waitForOngoingSync(appContext, effectiveLocalAuthority, effectiveCloudAuthority, 518 query.getIntentAction()); 519 520 return PickerMediaDatabaseUtil.queryMergedAlbumMedia( 521 albumId, 522 appContext, 523 syncController, 524 queryArgs, 525 effectiveLocalAuthority, 526 effectiveCloudAuthority 527 ); 528 } else { 529 waitForOngoingAlbumSync(appContext, query, effectiveLocalAuthority); 530 531 return PickerMediaDatabaseUtil.queryAlbumMedia( 532 appContext, 533 syncController, 534 query, 535 effectiveLocalAuthority, 536 effectiveCloudAuthority 537 ); 538 } 539 } 540 541 /** 542 * Returns a cursor with the Photo Picker search results media in response. 543 * 544 * @param queryArgs The arguments help us filter on the media query to yield the desired 545 * results. 546 * @param searchRequestID Identifier of the search request. 547 */ querySearchMedia( @onNull Context appContext, @NonNull Bundle queryArgs, int searchRequestID)548 public static Cursor querySearchMedia( 549 @NonNull Context appContext, 550 @NonNull Bundle queryArgs, 551 int searchRequestID) { 552 final SearchMediaQuery query = new SearchMediaQuery(queryArgs, searchRequestID); 553 554 // Validate query input 555 if (MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(query.getIntentAction())) { 556 throw new RuntimeException("Search feature cannot be enabled with Picker Choice"); 557 } 558 559 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 560 final String effectiveLocalAuthority = 561 query.getProviders().contains(syncController.getLocalProvider()) 562 ? syncController.getLocalProvider() 563 : null; 564 final String cloudAuthority = syncController 565 .getCloudProviderOrDefault(/* defaultValue */ null); 566 final String effectiveCloudAuthority = 567 syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority) 568 ? cloudAuthority 569 : null; 570 571 waitForOngoingSearchResultSync(effectiveLocalAuthority, effectiveCloudAuthority); 572 // TODO(b/361042632) resume sync if required 573 574 return SearchResultsDatabaseUtil.querySearchMedia( 575 syncController, 576 query, 577 effectiveLocalAuthority, 578 effectiveCloudAuthority 579 ); 580 } 581 582 /** 583 * Returns a cursor with the cached content of a media set in response 584 * @param queryArgs The arguments to filter and fetch media set content 585 */ queryMediaInMediaSet(@onNull Bundle queryArgs)586 public static Cursor queryMediaInMediaSet(@NonNull Bundle queryArgs) { 587 588 requireNonNull(queryArgs); 589 590 MediaInMediaSetSyncRequestParams requestParams = 591 new MediaInMediaSetSyncRequestParams(queryArgs); 592 MediaInMediaSetsQuery query = new MediaInMediaSetsQuery( 593 queryArgs, requestParams.getMediaSetPickerId() 594 ); 595 596 if (MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(query.getIntentAction())) { 597 throw new RuntimeException("Search feature cannot be enabled with PickerChoice. " 598 + "Can't query MediaSet content"); 599 } 600 601 PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 602 final Set<String> providers = new HashSet<>(query.getProviders()); 603 final String effectiveLocalAuthority = syncController 604 .getLocalProvider().equals(requestParams.getAuthority()) 605 ? requestParams.getAuthority() : null; 606 String currentCloudAuthority = syncController.getCloudProviderOrDefault( 607 /*defaultValue*/ null); 608 final String effectiveCloudAuthority = syncController 609 .shouldQueryCloudMediaSets(providers, currentCloudAuthority) 610 ? currentCloudAuthority : null; 611 612 waitForOngoingMediaInMediaSetSync(effectiveLocalAuthority, effectiveCloudAuthority); 613 614 return MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet( 615 syncController, query, effectiveLocalAuthority, effectiveCloudAuthority); 616 617 } 618 619 /** 620 * Get search suggestions for a given prefix from the cloud media provider and search history. 621 * In case cloud media provider is taking time in returning the suggestion results, we'll try to 622 * fallback on previously cached search results. 623 * 624 * @param appContext Application context. 625 * @param queryArgs The arguments help us filter on the media query to get the desired results. 626 * @param cancellationSignal CancellationSignal that indicates that the client has cancelled 627 * the suggestions request and the results are not needed anymore. 628 * @return A cursor with search suggestion data. 629 * See {@link PickerSQLConstants.SearchSuggestionsResponseColumns}. 630 */ querySearchSuggestions( @onNull Context appContext, @NonNull Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)631 static Cursor querySearchSuggestions( 632 @NonNull Context appContext, 633 @NonNull Bundle queryArgs, 634 @Nullable CancellationSignal cancellationSignal) { 635 // By default use ForkJoinPool.commonPool() to reduce resource usage instead of creating a 636 // custom pool. Its threads are slowly reclaimed during periods of non-use, and reinstated 637 // upon subsequent use. 638 return querySearchSuggestions(appContext, queryArgs, ForkJoinPool.commonPool(), 639 cancellationSignal); 640 } 641 642 /** 643 * Get search suggestions for a given prefix from the cloud media provider and search history. 644 * In case cloud media provider is taking time in returning the suggestion results, we'll try to 645 * fallback on previously cached search results. 646 * 647 * @param appContext Application context. 648 * @param queryArgs The arguments help us filter on the media query to get the desired results. 649 * @param executor The executor used to run async tasks. 650 * @param cancellationSignal CancellationSignal that indicates that the client has cancelled 651 * the suggestions request and the results are not needed anymore. 652 * @return A cursor with search suggestion data. 653 * See {@link PickerSQLConstants.SearchSuggestionsResponseColumns}. 654 */ querySearchSuggestions( @onNull Context appContext, @NonNull Bundle queryArgs, @NonNull Executor executor, @Nullable CancellationSignal cancellationSignal)655 static Cursor querySearchSuggestions( 656 @NonNull Context appContext, 657 @NonNull Bundle queryArgs, 658 @NonNull Executor executor, 659 @Nullable CancellationSignal cancellationSignal) { 660 final SearchSuggestionsQuery query = new SearchSuggestionsQuery(queryArgs); 661 662 // Attempt to fetch search suggestions from CMPs within the given timeout. 663 List<SearchSuggestion> cloudSearchSuggestions = new ArrayList<>(); 664 CompletableFuture<List<SearchSuggestion>> cloudSuggestionsFuture = 665 CompletableFuture.supplyAsync(() -> 666 getSuggestionsFromCloudProvider(appContext, query, cancellationSignal), 667 executor); 668 669 List<SearchSuggestion> localSearchSuggestions = new ArrayList<>(); 670 CompletableFuture<List<SearchSuggestion>> localSuggestionsFuture = 671 CompletableFuture.supplyAsync(() -> 672 getSuggestionsFromLocalProvider(appContext, query, cancellationSignal), 673 executor); 674 try { 675 localSearchSuggestions = localSuggestionsFuture.get( 676 /* timeout */ 200, TimeUnit.MILLISECONDS); 677 } catch (TimeoutException e) { 678 Log.e(TAG, "Could not get search suggestions from local provider on time"); 679 localSuggestionsFuture.cancel(/* mayInterruptIfRunning */ false); 680 } catch (RuntimeException | ExecutionException | InterruptedException e) { 681 Log.e(TAG, ("Something went wrong, " 682 + "could not fetch search results from the local provider"), e); 683 } 684 685 try { 686 cloudSearchSuggestions = cloudSuggestionsFuture.get( 687 /* timeout */ 1500, TimeUnit.MILLISECONDS); 688 cloudSuggestionsFuture.thenApplyAsync( 689 (suggestions) -> maybeCacheSearchSuggestions(query, suggestions), 690 executor); 691 } catch (TimeoutException e) { 692 Log.e(TAG, "Could not get search suggestions from cloud provider on time"); 693 694 // Only cancel suggestion request if the results don't need to be cached. 695 if (!query.isZeroState()) { 696 cloudSuggestionsFuture.cancel(/* mayInterruptIfRunning */ false); 697 } 698 } catch (RuntimeException | ExecutionException | InterruptedException e) { 699 Log.e(TAG, ("Something went wrong, " 700 + "could not fetch search results from the cloud provider"), e); 701 } 702 703 // Fallback to cached suggestions if required. 704 if (cloudSearchSuggestions.isEmpty()) { 705 Log.d(TAG, "Attempting to fallback on cached search suggestions"); 706 cloudSearchSuggestions = SearchSuggestionsDatabaseUtils.getCachedSuggestions( 707 PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase(), 708 query 709 ); 710 } 711 712 // Get History Suggestions 713 final List<SearchSuggestion> historySuggestions = 714 SearchSuggestionsDatabaseUtils.getHistorySuggestions( 715 PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase(), 716 query); 717 718 // Get Default Suggestions 719 final List<SearchSuggestion> defaultSuggestions = getDefaultSuggestions(); 720 721 // Merge suggestions in the order of priority 722 final List<SearchSuggestion> result = new ArrayList<>(); 723 result.addAll(historySuggestions); 724 result.addAll(cloudSearchSuggestions); 725 result.addAll(localSearchSuggestions); 726 result.addAll(defaultSuggestions); 727 728 // Remove extra suggestions if the result exceeds the limit. 729 if (result.size() > query.getLimit()) { 730 result.subList(result.size() - query.getLimit(), result.size()).clear(); 731 } 732 733 return suggestionsToCursor(result); 734 } 735 736 /** 737 * Queries the picker database and fetches the count of pre-granted media. Returns count of 738 * media either owned by the app or user has granted access to. 739 * 740 * @return a [Cursor] containing only one column [COLUMN_GRANTS_COUNT] which have a single 741 * row representing the count. 742 */ fetchCountForPreGrantedItems( @onNull Context appContext, @NonNull Bundle queryArgs)743 static Cursor fetchCountForPreGrantedItems( 744 @NonNull Context appContext, 745 @NonNull Bundle queryArgs) { 746 String[] projectionIn = new String[]{PROJECTION_GRANTS_COUNT}; 747 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 748 final SQLiteDatabase database = syncController.getDbFacade().getDatabase(); 749 750 waitForOngoingGrantsSync(appContext); 751 752 int packageUid = queryArgs.getInt(Intent.EXTRA_UID); 753 int userId = uidToUserId(packageUid); 754 String[] packageNames = getPackageNameFromUid(appContext, 755 packageUid); 756 757 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 758 if (isOwnedPhotosEnabled(packageUid)) { 759 waitForOngoingSync(appContext, syncController.getLocalProvider(), null, 760 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP); 761 762 qb.setTables(MEDIA_LEFT_JOIN_MEDIA_GRANTS_TABLE); 763 764 String packageSelectionForMediaGrants = getPackageSelectionWhereClause(packageNames, 765 MEDIA_GRANTS_TABLE).toString(); 766 String packageSelectionForMedia = getPackageSelectionWhereClause(packageNames, 767 MEDIA_TABLE).toString(); 768 769 /* 770 (media.owner_package_name IN (com.android.example) AND media._user_id = 0) OR 771 (media_grants.owner_package_name IN (com.android.example) AND media_grants._user_id = 0) 772 */ 773 String whereClause = String.format(Locale.ROOT, 774 "(%s AND %s.%s = %d) OR (%s AND %s.%s = %d)", 775 packageSelectionForMedia, 776 MEDIA_TABLE, MediaStore.Files.FileColumns._USER_ID, userId, 777 packageSelectionForMediaGrants, 778 MEDIA_GRANTS_TABLE, PACKAGE_USER_ID_COLUMN, userId); 779 780 qb.appendWhereStandalone(whereClause); 781 } else { 782 qb.setTables(MEDIA_GRANTS_TABLE); 783 addWhereClausesForPackageAndUserIdSelection(userId, packageNames, MEDIA_GRANTS_TABLE, 784 qb); 785 } 786 787 Cursor result = qb.query(database, projectionIn, null, 788 null, null, null, null); 789 return result; 790 } 791 792 /** 793 * Adds the clause to select rows based on calling packageName and userId. 794 */ addWhereClausesForPackageAndUserIdSelection(int userId, @NonNull String[] packageNames, String table, SQLiteQueryBuilder qb)795 public static void addWhereClausesForPackageAndUserIdSelection(int userId, 796 @NonNull String[] packageNames, String table, SQLiteQueryBuilder qb) { 797 // Add where clause for userId selection. 798 qb.appendWhereStandalone( 799 String.format(Locale.ROOT, 800 "%s.%s = %d", table, PACKAGE_USER_ID_COLUMN, userId)); 801 802 // Add where clause for package name selection. 803 Objects.requireNonNull(packageNames); 804 qb.appendWhereStandalone(getPackageSelectionWhereClause(packageNames, 805 table).toString()); 806 } 807 waitForOngoingSync( @onNull Context appContext, @Nullable String localAuthority, @Nullable String cloudAuthority, String intentAction)808 private static void waitForOngoingSync( 809 @NonNull Context appContext, 810 @Nullable String localAuthority, 811 @Nullable String cloudAuthority, String intentAction) { 812 // when the intent action is ACTION_USER_SELECT_IMAGES_FOR_APP, the flow should wait for 813 // the sync of grants and since this is a localOnly session. It should not wait or check 814 // cloud media. 815 boolean isUserSelectAction = MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals( 816 intentAction); 817 if (localAuthority != null) { 818 SyncCompletionWaiter.waitForSync( 819 getWorkManager(appContext), 820 SyncTrackerRegistry.getLocalSyncTracker(), 821 IMMEDIATE_LOCAL_SYNC_WORK_NAME 822 ); 823 if (isUserSelectAction) { 824 SyncCompletionWaiter.waitForSync( 825 getWorkManager(appContext), 826 SyncTrackerRegistry.getGrantsSyncTracker(), 827 IMMEDIATE_GRANTS_SYNC_WORK_NAME 828 ); 829 } 830 } 831 832 if (cloudAuthority != null && !isUserSelectAction) { 833 boolean syncIsComplete = SyncCompletionWaiter.waitForSyncWithTimeout( 834 SyncTrackerRegistry.getCloudSyncTracker(), 835 CLOUD_SYNC_TIMEOUT_MILLIS); 836 Log.i(TAG, "Finished waiting for cloud sync. Is cloud sync complete: " 837 + syncIsComplete); 838 } 839 } 840 waitForOngoingGrantsSync( @onNull Context appContext)841 private static void waitForOngoingGrantsSync( 842 @NonNull Context appContext) { 843 SyncCompletionWaiter.waitForSync( 844 getWorkManager(appContext), 845 SyncTrackerRegistry.getGrantsSyncTracker(), 846 IMMEDIATE_GRANTS_SYNC_WORK_NAME 847 ); 848 } 849 850 /** 851 * @param appContext The application context. 852 * @param query The AlbumMediaQuery object instance that tells us about the media query args. 853 * @param localAuthority The effective local authority that we need to consider for this 854 * transaction. If the local items should not be queries but the local 855 * authority has some value, the effective local authority would be null. 856 */ waitForOngoingAlbumSync( @onNull Context appContext, @NonNull AlbumMediaQuery query, @Nullable String localAuthority)857 private static void waitForOngoingAlbumSync( 858 @NonNull Context appContext, 859 @NonNull AlbumMediaQuery query, 860 @Nullable String localAuthority) { 861 boolean isLocal = localAuthority != null 862 && localAuthority.equals(query.getAlbumAuthority()); 863 SyncCompletionWaiter.waitForSyncWithTimeout( 864 SyncTrackerRegistry.getAlbumSyncTracker(isLocal), 865 /* timeoutInMillis */ 500); 866 } 867 868 /** 869 * @param appContext The application context. 870 * @param localAuthority The effective local authority that we need to consider for this 871 * transaction. If the local items should not be queried but the local 872 * authority has some value, the effective local authority would be null. 873 * @param cloudAuthority The effective cloud authority that we need to consider for this 874 * transaction. If the cloud items should not be queried but the cloud 875 * authority has some value, the effective cloud authority would be null. 876 */ waitForOngoingSearchResultSync( @ullable String localAuthority, @Nullable String cloudAuthority)877 private static void waitForOngoingSearchResultSync( 878 @Nullable String localAuthority, 879 @Nullable String cloudAuthority) { 880 final SearchState searchState = PickerSyncController.getInstanceOrThrow().getSearchState(); 881 882 if (localAuthority != null) { 883 Log.d(TAG, "Waiting for local search results"); 884 SyncCompletionWaiter.waitForSyncWithTimeout( 885 SyncTrackerRegistry.getLocalSearchSyncTracker(), /* timeoutInMillis */ 500); 886 } 887 888 if (cloudAuthority != null) { 889 Log.d(TAG, "Waiting for cloud search results"); 890 SyncCompletionWaiter.waitForSyncWithTimeout( 891 SyncTrackerRegistry.getCloudSearchSyncTracker(), /* timeoutInMillis */ 5000); 892 } 893 } 894 895 /** 896 * @param localAuthority The effective local authority that we need to consider for this 897 * transaction. If the local items should not be queried but the local 898 * authority has some value, the effective local authority would be null. 899 * @param cloudAuthority The effective cloud authority that we need to consider for this 900 * transaction. If the cloud items should not be queried but the cloud 901 * authority has some value, the effective cloud authority would be null. 902 */ waitForOngoingMediaInMediaSetSync( @ullable String localAuthority, @Nullable String cloudAuthority)903 private static void waitForOngoingMediaInMediaSetSync( 904 @Nullable String localAuthority, 905 @Nullable String cloudAuthority) { 906 if (localAuthority != null && cloudAuthority != null) { 907 Log.w(TAG, "Media sets sync can only happen with either the local provider or a " 908 + "cloud provider for a parent category. Please check the input providers."); 909 } 910 if (localAuthority != null) { 911 SyncCompletionWaiter.waitForSyncWithTimeout( 912 SyncTrackerRegistry.getLocalMediaInMediaSetTracker(), /*timeoutInMillis*/ 500); 913 } 914 if (cloudAuthority != null) { 915 SyncCompletionWaiter.waitForSyncWithTimeout( 916 SyncTrackerRegistry.getCloudMediaInMediaSetTracker(), /*timeoutInMillis*/ 5000); 917 } 918 } 919 920 /** 921 * @param localAuthority The effective local authority that we need to consider for this 922 * transaction. If the local items should not be queried but the local 923 * authority has some value, the effective local authority would be null. 924 * @param cloudAuthority The effective cloud authority that we need to consider for this 925 * transaction. If the cloud items should not be queried but the cloud 926 * authority has some value, the effective cloud authority would be null. 927 */ waitForOngoingMediaSetsSync( @ullable String localAuthority, @Nullable String cloudAuthority)928 private static void waitForOngoingMediaSetsSync( 929 @Nullable String localAuthority, 930 @Nullable String cloudAuthority) { 931 if (localAuthority != null && cloudAuthority != null) { 932 Log.w(TAG, "Media set contents sync can only happen with either the local provider or" 933 + " a cloud provider for a parent category. Please check the input providers."); 934 } 935 if (localAuthority != null) { 936 SyncCompletionWaiter.waitForSyncWithTimeout( 937 SyncTrackerRegistry.getLocalMediaSetsSyncTracker(), /*timeoutInMillis*/ 500); 938 } 939 if (cloudAuthority != null) { 940 SyncCompletionWaiter.waitForSyncWithTimeout( 941 SyncTrackerRegistry.getCloudMediaSetsSyncTracker(), /*timeoutInMillis*/ 2000); 942 } 943 } 944 945 /** 946 * Returns a clause that can be used to filter OWNER_PACKAGE_NAME_COLUMN using the input 947 * packageNames in a query. 948 */ getPackageSelectionWhereClause(String[] packageNames, String table)949 public static @NonNull StringBuilder getPackageSelectionWhereClause(String[] packageNames, 950 String table) { 951 StringBuilder packageSelection = new StringBuilder(); 952 String packageColumn = String.format( 953 Locale.ROOT, "%s.%s", table, OWNER_PACKAGE_NAME_COLUMN); 954 packageSelection.append(packageColumn).append(" IN (\'"); 955 956 String joinedPackageNames = String.join("\',\'", packageNames); 957 packageSelection.append(joinedPackageNames); 958 959 packageSelection.append("\')"); 960 return packageSelection; 961 } 962 getDefaultEmptyAlbum(@onNull String albumId)963 public static Cursor getDefaultEmptyAlbum(@NonNull String albumId) { 964 // Conform to the album response projection. Temporary code, this will change once we start 965 // caching album metadata. 966 final MatrixCursor result = new MatrixCursor(AlbumColumns.ALL_PROJECTION); 967 final String[] projectionValue = new String[]{ 968 /* albumId */ albumId, 969 /* dateTakenMillis */ Long.toString(Long.MAX_VALUE), 970 /* displayName */ albumId, 971 /* mediaId */ EMPTY_MEDIA_ID, 972 /* count */ "0", // This value is not used anymore 973 /* authority */ null, // Authority is populated in AlbumsCursorWrapper 974 }; 975 result.addRow(projectionValue); 976 return result; 977 } 978 979 /** 980 * Returns local albums in individial cursors mapped against their album id after fetching them 981 * from the local provider. 982 * 983 * @param appContext The application context. 984 * @param query Query arguments that will be used to filter albums. 985 * @param localAuthority Authority of the local media provider. 986 */ 987 @Nullable getLocalAlbumCursors( @onNull Context appContext, @NonNull MediaQuery query, @Nullable String localAuthority)988 private static Map<String, AlbumsCursorWrapper> getLocalAlbumCursors( 989 @NonNull Context appContext, 990 @NonNull MediaQuery query, 991 @Nullable String localAuthority) { 992 if (localAuthority == null) { 993 Log.d(TAG, "Cannot fetch local albums when local authority is null."); 994 return null; 995 } 996 997 final Cursor localAlbumsCursor = 998 getAlbumsCursorFromProvider(appContext, query, localAuthority); 999 1000 final Map<String, AlbumsCursorWrapper> localAlbumsMap = new HashMap<>(); 1001 if (localAlbumsCursor != null && localAlbumsCursor.moveToFirst()) { 1002 do { 1003 try { 1004 final String albumId = 1005 localAlbumsCursor.getString( 1006 localAlbumsCursor.getColumnIndex(AlbumColumns.ID)); 1007 final MatrixCursor albumCursor = 1008 new MatrixCursor(localAlbumsCursor.getColumnNames()); 1009 MatrixCursor.RowBuilder builder = albumCursor.newRow(); 1010 for (String columnName : localAlbumsCursor.getColumnNames()) { 1011 final int columnIndex = localAlbumsCursor.getColumnIndex(columnName); 1012 switch (localAlbumsCursor.getType(columnIndex)) { 1013 case Cursor.FIELD_TYPE_INTEGER: 1014 builder.add(columnName, localAlbumsCursor.getInt(columnIndex)); 1015 break; 1016 case Cursor.FIELD_TYPE_FLOAT: 1017 builder.add(columnName, localAlbumsCursor.getFloat(columnIndex)); 1018 break; 1019 case Cursor.FIELD_TYPE_BLOB: 1020 builder.add(columnName, localAlbumsCursor.getBlob(columnIndex)); 1021 break; 1022 case Cursor.FIELD_TYPE_NULL: 1023 builder.add(columnName, null); 1024 break; 1025 case Cursor.FIELD_TYPE_STRING: 1026 builder.add(columnName, localAlbumsCursor.getString(columnIndex)); 1027 break; 1028 default: 1029 throw new IllegalArgumentException( 1030 "Could not recognize column type " 1031 + localAlbumsCursor.getType(columnIndex)); 1032 } 1033 } 1034 localAlbumsMap.put( 1035 albumId, 1036 new AlbumsCursorWrapper(albumCursor, 1037 /* coverAuthority */ localAuthority, 1038 /* localAuthority */ localAuthority) 1039 ); 1040 } catch (RuntimeException e) { 1041 Log.e(TAG, 1042 "Could not read album cursor values received from local provider", e); 1043 } 1044 } while(localAlbumsCursor.moveToNext()); 1045 } 1046 1047 // Close localAlbumsCursor because it's data was copied into new Cursor(s) and it won't 1048 // be used again. 1049 if (localAlbumsCursor != null) localAlbumsCursor.close(); 1050 1051 // Always show Camera album. 1052 if (!localAlbumsMap.containsKey(AlbumColumns.ALBUM_ID_CAMERA)) { 1053 localAlbumsMap.put( 1054 AlbumColumns.ALBUM_ID_CAMERA, 1055 new AlbumsCursorWrapper( 1056 getDefaultEmptyAlbum(AlbumColumns.ALBUM_ID_CAMERA), 1057 /* albumAuthority */ localAuthority, 1058 /* localAuthority */ localAuthority) 1059 ); 1060 } 1061 1062 return localAlbumsMap; 1063 } 1064 1065 /** 1066 * Returns cloud albums cursor after fetching them from the local provider. 1067 * 1068 * @param appContext The application context. 1069 * @param query Query arguments that will be used to filter albums. 1070 * @param localAuthority Authority of the local media provider. 1071 * @param cloudAuthority Authority of the cloud media provider. 1072 */ 1073 @Nullable getCloudAlbumsCursor( @onNull Context appContext, @NonNull MediaQuery query, @Nullable String localAuthority, @Nullable String cloudAuthority)1074 private static AlbumsCursorWrapper getCloudAlbumsCursor( 1075 @NonNull Context appContext, 1076 @NonNull MediaQuery query, 1077 @Nullable String localAuthority, 1078 @Nullable String cloudAuthority) { 1079 if (cloudAuthority == null) { 1080 Log.d(TAG, "Cannot fetch cloud albums when cloud authority is null."); 1081 return null; 1082 } 1083 1084 Log.d(TAG, "Fetching albums from CMP " + cloudAuthority); 1085 final Cursor cursor = getAlbumsCursorFromProvider(appContext, query, cloudAuthority); 1086 1087 Log.d(TAG, "Received albums from CMP " + cloudAuthority); 1088 return cursor == null 1089 ? null 1090 : new AlbumsCursorWrapper(cursor, cloudAuthority, localAuthority); 1091 } 1092 1093 /** 1094 * Returns {@link AlbumsCursorWrapper} object that wraps the albums cursor response from the 1095 * provider. 1096 * 1097 * @param appContext The application context. 1098 * @param query Query arguments that will be used to filter albums. 1099 * @param providerAuthority Authority of the cloud media provider. 1100 */ 1101 @Nullable getAlbumsCursorFromProvider( @onNull Context appContext, @NonNull MediaQuery query, @NonNull String providerAuthority)1102 private static Cursor getAlbumsCursorFromProvider( 1103 @NonNull Context appContext, 1104 @NonNull MediaQuery query, 1105 @NonNull String providerAuthority) { 1106 return appContext.getContentResolver().query( 1107 getAlbumUri(providerAuthority), 1108 /* projection */ null, 1109 query.prepareCMPQueryArgs(), 1110 /* cancellationSignal */ null); 1111 } 1112 1113 /** 1114 * @param appContext Application context. 1115 * @param query Query arguments that will be used to filter categories. 1116 * @param cloudAuthority Effective cloud authority from which cloud categories should be 1117 * fetched. This could be null. 1118 * @param cancellationSignal CancellationSignal object that notifies that the request has been 1119 * cancelled. 1120 * @return Cursor with Categories from the cloud provider. Returns null if an error occurs in 1121 * fetching the categories. 1122 */ 1123 @Nullable getCloudCategories( @onNull Context appContext, @NonNull MediaQuery query, @Nullable String cloudAuthority, @NonNull PickerSyncController syncController, @Nullable CancellationSignal cancellationSignal)1124 private static Cursor getCloudCategories( 1125 @NonNull Context appContext, 1126 @NonNull MediaQuery query, 1127 @Nullable String cloudAuthority, 1128 @NonNull PickerSyncController syncController, 1129 @Nullable CancellationSignal cancellationSignal) { 1130 try { 1131 if (cloudAuthority == null) { 1132 Log.d(TAG, "Cannot fetch cloud categories when cloud authority is null."); 1133 return null; 1134 } 1135 1136 try { 1137 if (syncController.isFullSyncPending(cloudAuthority, /* isLocal */ false)) { 1138 Log.d(TAG, "Don't return cloud categories when full sync is pending."); 1139 return null; 1140 } 1141 } catch (RequestObsoleteException | RuntimeException e) { 1142 Log.e(TAG, "Could not check if full sync is pending. " 1143 + "Not returning cloud categories", e); 1144 return null; 1145 } 1146 1147 final PickerSearchProviderClient searchClient = PickerSearchProviderClient.create( 1148 appContext, cloudAuthority); 1149 if (syncController.getCategoriesState().areCategoriesEnabled( 1150 appContext, cloudAuthority)) { 1151 Log.d(TAG, "Media categories feature is enabled. Fetching cloud categories."); 1152 return searchClient.fetchMediaCategoriesFromCmp( 1153 /* parentCategoryId */ null, 1154 query.prepareCMPQueryArgs(), 1155 /* cancellationSignal */ cancellationSignal); 1156 } 1157 } catch (RuntimeException e) { 1158 Log.e(TAG, "Could not fetch cloud categories.", e); 1159 } 1160 1161 return null; 1162 } 1163 1164 /** 1165 * @return a cursor with the available providers. 1166 */ 1167 @NonNull queryAvailableProviders(@onNull Context context)1168 public static Cursor queryAvailableProviders(@NonNull Context context) { 1169 try { 1170 final PackageManager packageManager = context.getPackageManager(); 1171 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1172 final String[] columnNames = Arrays 1173 .stream(PickerSQLConstants.AvailableProviderResponse.values()) 1174 .map(PickerSQLConstants.AvailableProviderResponse::getColumnName) 1175 .toArray(String[]::new); 1176 final MatrixCursor matrixCursor = new MatrixCursor(columnNames, /*initialCapacity */ 2); 1177 final String localAuthority = syncController.getLocalProvider(); 1178 final ProviderInfo localProviderInfo = packageManager.resolveContentProvider( 1179 localAuthority, /* flags */ 0); 1180 final String localProviderLabel = 1181 String.valueOf(localProviderInfo.loadLabel(packageManager)); 1182 addAvailableProvidersToCursor( 1183 matrixCursor, 1184 localAuthority, 1185 MediaSource.LOCAL, 1186 Process.myUid(), 1187 localProviderLabel 1188 ); 1189 1190 final String cloudAuthority = 1191 syncController.getCloudProviderOrDefault(/* defaultValue */ null); 1192 if (syncController.shouldQueryCloudMedia(cloudAuthority)) { 1193 final ProviderInfo cloudProviderInfo = requireNonNull( 1194 packageManager.resolveContentProvider(cloudAuthority, /* flags */ 0)); 1195 final int uid = packageManager.getPackageUid( 1196 cloudProviderInfo.packageName, 1197 /* flags */ 0 1198 ); 1199 final String cloudProviderLabel = 1200 String.valueOf(cloudProviderInfo.loadLabel(packageManager)); 1201 addAvailableProvidersToCursor( 1202 matrixCursor, 1203 cloudAuthority, 1204 MediaSource.REMOTE, 1205 uid, 1206 cloudProviderLabel 1207 ); 1208 } 1209 1210 return matrixCursor; 1211 } catch (IllegalStateException | NameNotFoundException e) { 1212 throw new RuntimeException("Unexpected internal error occurred", e); 1213 } 1214 } 1215 1216 /** 1217 * @return a cursor with the Collection Info for all the available providers. 1218 */ queryCollectionInfo()1219 public static Cursor queryCollectionInfo() { 1220 try { 1221 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1222 final String[] columnNames = Arrays 1223 .stream(PickerSQLConstants.CollectionInfoResponse.values()) 1224 .map(PickerSQLConstants.CollectionInfoResponse::getColumnName) 1225 .toArray(String[]::new); 1226 final MatrixCursor matrixCursor = new MatrixCursor(columnNames, /*initialCapacity */ 2); 1227 Bundle extras = new Bundle(); 1228 matrixCursor.setExtras(extras); 1229 final ProviderCollectionInfo localCollectionInfo = 1230 syncController.getLocalProviderLatestCollectionInfo(); 1231 addCollectionInfoToCursor( 1232 matrixCursor, 1233 localCollectionInfo 1234 ); 1235 1236 final ProviderCollectionInfo cloudCollectionInfo = 1237 syncController.getCloudProviderLatestCollectionInfo(); 1238 if (cloudCollectionInfo != null 1239 && syncController.shouldQueryCloudMedia(cloudCollectionInfo.getAuthority())) { 1240 addCollectionInfoToCursor( 1241 matrixCursor, 1242 cloudCollectionInfo 1243 ); 1244 } 1245 1246 return matrixCursor; 1247 } catch (IllegalStateException e) { 1248 throw new RuntimeException("Unexpected internal error occurred", e); 1249 } 1250 } 1251 addAvailableProvidersToCursor( @onNull MatrixCursor cursor, @NonNull String authority, @NonNull MediaSource source, @UserIdInt int uid, @Nullable String displayName)1252 private static void addAvailableProvidersToCursor( 1253 @NonNull MatrixCursor cursor, 1254 @NonNull String authority, 1255 @NonNull MediaSource source, 1256 @UserIdInt int uid, 1257 @Nullable String displayName) { 1258 cursor.newRow() 1259 .add(PickerSQLConstants.AvailableProviderResponse.AUTHORITY.getColumnName(), 1260 authority) 1261 .add(PickerSQLConstants.AvailableProviderResponse.MEDIA_SOURCE.getColumnName(), 1262 source.name()) 1263 .add(PickerSQLConstants.AvailableProviderResponse.UID.getColumnName(), uid) 1264 .add(PickerSQLConstants.AvailableProviderResponse.DISPLAY_NAME.getColumnName(), 1265 displayName); 1266 } 1267 addCollectionInfoToCursor( @onNull MatrixCursor cursor, @NonNull ProviderCollectionInfo providerCollectionInfo)1268 private static void addCollectionInfoToCursor( 1269 @NonNull MatrixCursor cursor, 1270 @NonNull ProviderCollectionInfo providerCollectionInfo) { 1271 if (providerCollectionInfo != null) { 1272 cursor.newRow() 1273 .add(PickerSQLConstants.CollectionInfoResponse.AUTHORITY.getColumnName(), 1274 providerCollectionInfo.getAuthority()) 1275 .add(PickerSQLConstants.CollectionInfoResponse.COLLECTION_ID.getColumnName(), 1276 providerCollectionInfo.getCollectionId()) 1277 .add(PickerSQLConstants.CollectionInfoResponse.ACCOUNT_NAME.getColumnName(), 1278 providerCollectionInfo.getAccountName()); 1279 1280 Bundle extras = cursor.getExtras(); 1281 extras.putParcelable(providerCollectionInfo.getAuthority(), 1282 providerCollectionInfo.getAccountConfigurationIntent()); 1283 } 1284 } 1285 1286 /** 1287 * @return a Bundle with the details of the requested cloud provider. 1288 */ getCloudProviderDetails(Bundle queryArgs)1289 public static Bundle getCloudProviderDetails(Bundle queryArgs) { 1290 throw new UnsupportedOperationException("This method is not implemented yet."); 1291 } 1292 1293 /** 1294 * Returns a cursor for media filtered by ids based on input URIs. 1295 */ queryMediaForPreSelection(@onNull Context appContext, Bundle queryArgs)1296 public static Cursor queryMediaForPreSelection(@NonNull Context appContext, Bundle queryArgs) { 1297 final MediaQueryForPreSelection query = new MediaQueryForPreSelection(queryArgs); 1298 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1299 final String effectiveLocalAuthority = 1300 query.getProviders().contains(syncController.getLocalProvider()) 1301 ? syncController.getLocalProvider() 1302 : null; 1303 final String cloudAuthority = syncController 1304 .getCloudProviderOrDefault(/* defaultValue */ null); 1305 final String effectiveCloudAuthority = 1306 syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority) 1307 ? cloudAuthority 1308 : null; 1309 waitForOngoingSync(appContext, effectiveLocalAuthority, effectiveCloudAuthority, 1310 query.getIntentAction()); 1311 1312 query.processUrisForSelection(query.getPreSelectionUris(), effectiveLocalAuthority, 1313 effectiveCloudAuthority, effectiveCloudAuthority == null, appContext, 1314 query.getCallingPackageUid()); 1315 1316 return PickerMediaDatabaseUtil.queryPreSelectedMedia( 1317 appContext, 1318 syncController, 1319 query, 1320 effectiveLocalAuthority, 1321 effectiveCloudAuthority 1322 ); 1323 } 1324 1325 /** 1326 * Handle Picker application's request to initialize search request media. If a new search 1327 * request id needs to be created, return a Bundle with the search request Id. 1328 * 1329 * Also trigger search results sync with the providers and saves the incoming search request in 1330 * the search history table if this is a new request. 1331 * 1332 * By default use ForkJoinPool.commonPool() for small background tasks to reduce resource 1333 * usage instead of creating a custom pool. Its threads are slowly reclaimed during periods 1334 * of non-use, and reinstated upon subsequent use. 1335 * 1336 * @param appContext Application context. 1337 * @param extras Bundle with input parameters. 1338 * @return a response Bundle. 1339 */ 1340 @NonNull handleSearchResultsInit( @onNull Context appContext, @NonNull Bundle extras)1341 public static Bundle handleSearchResultsInit( 1342 @NonNull Context appContext, 1343 @NonNull Bundle extras) { 1344 final int defaultSearchRequestId = -1; 1345 final int inputSearchRequestId = extras.getInt("search_request_id", defaultSearchRequestId); 1346 final WorkManager workManager = getWorkManager(appContext); 1347 1348 if (inputSearchRequestId == defaultSearchRequestId) { 1349 Log.d(TAG, "New search request id needs to be created"); 1350 1351 return handleNewSearchRequest(appContext, extras, 1352 ForkJoinPool.commonPool(), workManager); 1353 } else { 1354 Log.d(TAG, "Search request id already exists. Ensure that sync is complete for the " 1355 + "search request id."); 1356 1357 return ensureSearchResultsSynced(appContext, extras, workManager); 1358 } 1359 } 1360 1361 /** 1362 * Handle Picker application's request to create a new search request and return a Bundle with 1363 * the search request Id. 1364 * Also trigger search results sync with the providers and saves the incoming search request in 1365 * the search history table. 1366 * 1367 * @param appContext Application context. 1368 * @param extras Bundle with input parameters. 1369 * @param executor Executor to asynchronously save the request as search history in database. 1370 * @param workManager An instance of {@link WorkManager} 1371 * @return a response Bundle. 1372 */ 1373 @NonNull handleNewSearchRequest(@onNull Context appContext, @NonNull Bundle extras, @NonNull Executor executor, @NonNull WorkManager workManager)1374 public static Bundle handleNewSearchRequest(@NonNull Context appContext, 1375 @NonNull Bundle extras, 1376 @NonNull Executor executor, 1377 @NonNull WorkManager workManager) { 1378 requireNonNull(extras); 1379 Log.d(TAG, "Received a new search request: " + extras); 1380 1381 final SearchRequest searchRequest = SearchRequest.create(extras); 1382 final SQLiteDatabase database = PickerSyncController.getInstanceOrThrow().getDbFacade() 1383 .getDatabase(); 1384 1385 SearchRequestDatabaseUtil.saveSearchRequest(database, searchRequest); 1386 final int searchRequestId = 1387 SearchRequestDatabaseUtil.getSearchRequestID(database, searchRequest); 1388 1389 if (searchRequestId == -1) { 1390 throw new RuntimeException("Could not create search request!"); 1391 } else { 1392 // Asynchronously save data in search history table. 1393 CompletableFuture<Boolean> ignored = CompletableFuture.supplyAsync( 1394 () -> SearchSuggestionsDatabaseUtils.saveSearchHistory(database, searchRequest), 1395 executor); 1396 1397 // Schedule search results sync 1398 final Set<String> providers = new HashSet<>( 1399 Objects.requireNonNull(extras.getStringArrayList("providers"))); 1400 scheduleSearchResultsSync(appContext, searchRequest, searchRequestId, providers, 1401 workManager); 1402 1403 Log.d(TAG, "Returning search request id: " + searchRequestId); 1404 return getSearchRequestInitResponse(searchRequestId); 1405 } 1406 } 1407 1408 /** 1409 * Ensure that the search results are synced for the given search request id. If not, 1410 * start or resume the sync for the search results. Both of these aspects are taken care of by 1411 * scheduling a work request. 1412 * 1413 * @param appContext Application context. 1414 * @param extras Bundle with input parameters. 1415 * @param workManager An instance of {@link WorkManager} 1416 * @return a response Bundle. 1417 */ 1418 @NonNull ensureSearchResultsSynced(@onNull Context appContext, @NonNull Bundle extras, @NonNull WorkManager workManager)1419 public static Bundle ensureSearchResultsSynced(@NonNull Context appContext, 1420 @NonNull Bundle extras, 1421 @NonNull WorkManager workManager) { 1422 requireNonNull(extras); 1423 Log.d(TAG, "Received a previously known search request again: " + extras); 1424 1425 final int searchRequestId = extras.getInt("search_request_id", -1); 1426 final SearchRequest searchRequest = SearchRequest.create(extras); 1427 1428 // Schedule search results sync with REPLACE policy. This takes care of cancelling any 1429 // existing search results sync that might be obsolete. 1430 final Set<String> providers = new HashSet<>( 1431 Objects.requireNonNull(extras.getStringArrayList("providers"))); 1432 scheduleSearchResultsSync(appContext, searchRequest, searchRequestId, providers, 1433 workManager); 1434 1435 return getSearchRequestInitResponse(searchRequestId); 1436 } 1437 1438 /** 1439 * Handles Photopicker's request to trigger a sync for media items in a media set 1440 * based on whether the provider implements search categories and media sets 1441 * @param extras Bundle with all input parameters 1442 * @param appContext The application context 1443 */ triggerMediaSyncForMediaSet( @onNull Bundle extras, @NonNull Context appContext)1444 public static void triggerMediaSyncForMediaSet( 1445 @NonNull Bundle extras, @NonNull Context appContext) { 1446 requireNonNull(extras); 1447 requireNonNull(appContext); 1448 triggerMediaSyncForMediaSet(extras, appContext, getWorkManager(appContext)); 1449 } 1450 1451 /** 1452 * Handles Photopicker's request to trigger a sync for media items in a media set 1453 * based on whether the provider implements search categories and media sets 1454 * @param extras Bundle with all input parameters 1455 * @param appContext The application context 1456 * @param workManager An instance of {@link WorkManager} 1457 */ triggerMediaSyncForMediaSet( @onNull Bundle extras, @NonNull Context appContext, @NonNull WorkManager workManager)1458 public static void triggerMediaSyncForMediaSet( 1459 @NonNull Bundle extras, @NonNull Context appContext, @NonNull WorkManager workManager) { 1460 requireNonNull(extras); 1461 requireNonNull(appContext); 1462 requireNonNull(workManager); 1463 MediaInMediaSetSyncRequestParams requestParams = 1464 new MediaInMediaSetSyncRequestParams(extras); 1465 final Set<String> providers = new HashSet<>( 1466 Objects.requireNonNull(extras.getStringArrayList("providers"))); 1467 scheduleMediaInMediaSetSync( 1468 requestParams, appContext, workManager, providers); 1469 } 1470 1471 /** 1472 * Schedules a sync of media items in the given media set for the local or cloud provider if t 1473 * he corresponding provider implements Categories and MediaSets. 1474 * @param context The application context 1475 * @param requestParams Wrapper object to hold all media in media set sync parameters 1476 * @param providers Set of available providers 1477 * @param workManager An instance of {@link WorkManager} 1478 */ scheduleMediaInMediaSetSync( @onNull MediaInMediaSetSyncRequestParams requestParams, @NonNull Context context, @NonNull WorkManager workManager, @NonNull Set<String> providers)1479 private static void scheduleMediaInMediaSetSync( 1480 @NonNull MediaInMediaSetSyncRequestParams requestParams, @NonNull Context context, 1481 @NonNull WorkManager workManager, @NonNull Set<String> providers) { 1482 1483 PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1484 PickerSyncManager syncManager = new PickerSyncManager(workManager, context); 1485 int syncSource = Objects.equals(requestParams.getAuthority(), 1486 syncController.getLocalProvider()) 1487 ? SYNC_LOCAL_ONLY : SYNC_CLOUD_ONLY; 1488 1489 // Sync MediaSet content only if the media sets can actually be queried 1490 if (syncSource == SYNC_LOCAL_ONLY && syncController.shouldQueryLocalMediaSets(providers)) { 1491 syncManager.syncMediaInMediaSetForProvider( 1492 requestParams, SYNC_LOCAL_ONLY); 1493 } else if (syncController.shouldQueryCloudMediaSets( 1494 providers, requestParams.getAuthority())) { 1495 syncManager.syncMediaInMediaSetForProvider( 1496 requestParams, SYNC_CLOUD_ONLY); 1497 } else { 1498 Log.e(TAG, "Unidentified provider authority: " + requestParams.getAuthority() 1499 + " skipping MediaSet content sync."); 1500 } 1501 } 1502 1503 1504 1505 /** 1506 * Handles Photopicker's request to trigger a sync for media sets for the given category 1507 * based on whether the providers implement search categories. 1508 * @param extras Bundle with all input parameters 1509 * @param appContext The application context 1510 */ triggerMediaSetsSync( @onNull Bundle extras, @NonNull Context appContext)1511 public static void triggerMediaSetsSync( 1512 @NonNull Bundle extras, @NonNull Context appContext) { 1513 requireNonNull(extras); 1514 requireNonNull(appContext); 1515 triggerMediaSetsSync(extras, appContext, getWorkManager(appContext)); 1516 } 1517 1518 /** 1519 * Handles Photopicker's request to trigger a sync for media sets for the given category 1520 * based on whether the providers implement search categories. 1521 * @param extras Bundle with all input parameters 1522 * @param appContext The application context 1523 * @param workManager An instance of {@link WorkManager} 1524 */ triggerMediaSetsSync( @onNull Bundle extras, @NonNull Context appContext, @NonNull WorkManager workManager)1525 public static void triggerMediaSetsSync( 1526 @NonNull Bundle extras, @NonNull Context appContext, @NonNull WorkManager workManager) { 1527 1528 requireNonNull(workManager); 1529 MediaSetsSyncRequestParams mediaSetsSyncRequestParams = 1530 new MediaSetsSyncRequestParams(extras); 1531 final Set<String> providers = new HashSet<>( 1532 Objects.requireNonNull(extras.getStringArrayList("providers"))); 1533 1534 scheduleMediaSetsSync(appContext, mediaSetsSyncRequestParams, providers, workManager); 1535 } 1536 1537 /** 1538 * Handle cloud media queries being disabled. This method is called from the critical path of 1539 * updating the PickerDBFacade cloud provider. This method should not take a long time to 1540 * execute and it should ensure that unexpected exceptions/failures don't cause any disruption. 1541 * @param context The application context. 1542 */ handleCloudMediaReset(Context context)1543 public static void handleCloudMediaReset(Context context) { 1544 try { 1545 if (Flags.enablePhotopickerSearch()) { 1546 Log.d(TAG, "Cloud media reset detected. " 1547 + "Clear search results cache that has synced with a cloud media provider"); 1548 1549 final PickerSyncManager syncManager = 1550 new PickerSyncManager(getWorkManager(context), context); 1551 final String cloudAuthority = PickerSyncController.getInstanceOrThrow() 1552 .getCloudProviderOrDefault(null); 1553 syncManager.resetCloudSearchCache(cloudAuthority); 1554 } 1555 } catch (Exception e) { 1556 Log.e(TAG, "Unexpected error occurred in handleCloudMediaReset", e); 1557 } 1558 } 1559 1560 /** 1561 * @param context the application context. 1562 * @return a bundle with the list of available provider authorities that support the 1563 * search feature. If no providers are available, return an empty list in the bundle. 1564 */ 1565 @NonNull getSearchProviders(@onNull Context context)1566 public static Bundle getSearchProviders(@NonNull Context context) { 1567 Log.d(TAG, "Calculating available search providers."); 1568 1569 requireNonNull(context); 1570 1571 // Check the state of cloud and local search. 1572 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1573 final String cloudProvider = syncController.getCloudProviderOrDefault(null); 1574 final boolean isCloudSearchEnabled = 1575 syncController.getSearchState().isCloudSearchEnabled(context, cloudProvider); 1576 final boolean isLocalSearchEnabled = syncController.getSearchState().isLocalSearchEnabled(); 1577 1578 // Prepare a bundle response with the result. 1579 final ArrayList<String> searchProviderAuthorities = new ArrayList<>(); 1580 if (isCloudSearchEnabled) searchProviderAuthorities.add(cloudProvider); 1581 if (isLocalSearchEnabled) searchProviderAuthorities.add(syncController.getLocalProvider()); 1582 1583 final Bundle result = new Bundle(); 1584 result.putStringArrayList( 1585 PickerSQLConstants.EXTRA_SEARCH_PROVIDER_AUTHORITIES, searchProviderAuthorities); 1586 Log.d(TAG, "Available search providers are: " + result); 1587 return result; 1588 } 1589 1590 /** 1591 * Schedules MediaSets sync for both local and cloud provider if the corresponding 1592 * providers implement Categories. 1593 * @param appContext The application context 1594 * @param requestParams Wrapper object to hold all media set sync parameters 1595 * @param providers List of available providers 1596 * @param workManager An instance of {@link WorkManager} 1597 */ scheduleMediaSetsSync( @onNull Context appContext, @NonNull MediaSetsSyncRequestParams requestParams, @NonNull Set<String> providers, @NonNull WorkManager workManager)1598 private static void scheduleMediaSetsSync( 1599 @NonNull Context appContext, @NonNull MediaSetsSyncRequestParams requestParams, 1600 @NonNull Set<String> providers, @NonNull WorkManager workManager) { 1601 1602 final PickerSyncManager syncManager = new PickerSyncManager(workManager, appContext); 1603 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1604 int syncSource = syncController.getLocalProvider().equals(requestParams.getAuthority()) 1605 ? SYNC_LOCAL_ONLY : SYNC_CLOUD_ONLY; 1606 1607 // Schedule local sync only if the provider holds local authority 1608 if (syncSource == SYNC_LOCAL_ONLY && syncController.shouldQueryLocalMediaSets(providers)) { 1609 syncManager.syncMediaSetsForProvider(requestParams, SYNC_LOCAL_ONLY); 1610 } else if (syncController.shouldQueryCloudMediaSets( 1611 providers, requestParams.getAuthority())) { 1612 // Schedule cloud sync otherwise 1613 syncManager.syncMediaSetsForProvider(requestParams, SYNC_CLOUD_ONLY); 1614 } else { 1615 Log.e(TAG, "Unrecognised provider authority received for MediaSetSync, skipping"); 1616 } 1617 } 1618 1619 /** 1620 * Schedules search results sync for the incoming search request with local or cloud providers, 1621 * or both. 1622 * 1623 * @param appContext Application context. 1624 * @param searchRequest Search request for which search results need to be synced. 1625 * @param searchRequestId Identifier of the search request. 1626 * @param extras Bundle with input parameters. 1627 * @param workManager An instance of {@link WorkManager} 1628 */ scheduleSearchResultsSync( @onNull Context appContext, @NonNull SearchRequest searchRequest, int searchRequestId, @NonNull Set<String> providers, WorkManager workManager)1629 private static void scheduleSearchResultsSync( 1630 @NonNull Context appContext, 1631 @NonNull SearchRequest searchRequest, 1632 int searchRequestId, 1633 @NonNull Set<String> providers, 1634 WorkManager workManager) { 1635 final PickerSyncManager syncManager = new PickerSyncManager(workManager, appContext); 1636 1637 final boolean localSyncWasScheduled = scheduleSearchSyncWithLocalProvider( 1638 searchRequest, searchRequestId, syncManager, providers); 1639 final boolean cloudSyncWasScheduled = scheduleSearchSyncWithCloudProvider( 1640 searchRequest, searchRequestId, syncManager, providers); 1641 1642 if (localSyncWasScheduled || cloudSyncWasScheduled) { 1643 syncManager.delayedResetSearchCache(); 1644 } 1645 } 1646 1647 /** 1648 * Schedules search results sync for the incoming search request with local provider if local 1649 * search is enabled. 1650 * 1651 * @param searchRequest Search request for which search results need to be synced. 1652 * @param searchRequestId Identifier of the search request. 1653 * @param syncManager An instance of PickerSyncManager that helps us schedule work manager 1654 * sync requests. 1655 * @param providers Set of valid providers we can sync search results from. 1656 * @return True if the sync was schedules, else returns false. 1657 */ scheduleSearchSyncWithLocalProvider( @onNull SearchRequest searchRequest, int searchRequestId, @NonNull PickerSyncManager syncManager, @NonNull Set<String> providers)1658 private static boolean scheduleSearchSyncWithLocalProvider( 1659 @NonNull SearchRequest searchRequest, 1660 int searchRequestId, 1661 @NonNull PickerSyncManager syncManager, 1662 @NonNull Set<String> providers) { 1663 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1664 1665 if (!syncController.shouldQueryLocalMediaForSearch(providers)) { 1666 Log.d(TAG, "Search is not enabled for the current local authority. " 1667 + "Not syncing search results with local provider for request id " 1668 + searchRequestId); 1669 return false; 1670 } 1671 1672 if (searchRequest instanceof SearchSuggestionRequest) { 1673 final SearchSuggestion suggestion = 1674 ((SearchSuggestionRequest) searchRequest).getSearchSuggestion(); 1675 if (suggestion.getSearchSuggestionType() == SEARCH_SUGGESTION_ALBUM) { 1676 if (!syncController.getLocalProvider().equals(suggestion.getAuthority())) { 1677 Log.d(TAG, "Album search suggestion does not belong to local provider. " 1678 + "Not syncing search results with local provider for request id " 1679 + searchRequestId); 1680 return false; 1681 } 1682 } 1683 } 1684 1685 Log.d(TAG, "Scheduling search results sync with local provider: " + searchRequestId); 1686 syncManager.syncSearchResultsForProvider( 1687 searchRequestId, 1688 SYNC_LOCAL_ONLY, 1689 syncController.getLocalProvider()); 1690 return true; 1691 } 1692 1693 /** 1694 * Schedules search results sync for the incoming search request with cloud provider if cloud 1695 * search is enabled. 1696 * 1697 * @param searchRequest Search request for which search results need to be synced. 1698 * @param searchRequestId Identifier of the search request. 1699 * @param syncManager An instance of PickerSyncManager that helps us schedule work manager 1700 * sync requests. 1701 * @param providers Set of valid providers we can sync search results from. 1702 * @return True if the sync was schedules, else returns false. 1703 */ scheduleSearchSyncWithCloudProvider( @onNull SearchRequest searchRequest, int searchRequestId, @NonNull PickerSyncManager syncManager, @NonNull Set<String> providers)1704 private static boolean scheduleSearchSyncWithCloudProvider( 1705 @NonNull SearchRequest searchRequest, 1706 int searchRequestId, 1707 @NonNull PickerSyncManager syncManager, 1708 @NonNull Set<String> providers) { 1709 final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); 1710 final String cloudAuthority = 1711 syncController.getCloudProviderOrDefault(/* defaultValue */ null); 1712 1713 if (!syncController.shouldQueryCloudMediaForSearch(providers, cloudAuthority)) { 1714 Log.d(TAG, "Search is not enabled for the current cloud authority. " 1715 + "Not syncing search results with cloud provider for request id " 1716 + searchRequestId); 1717 return false; 1718 } 1719 1720 if (searchRequest instanceof SearchSuggestionRequest) { 1721 final SearchSuggestion suggestion = 1722 ((SearchSuggestionRequest) searchRequest).getSearchSuggestion(); 1723 if (suggestion.getSearchSuggestionType() == SEARCH_SUGGESTION_ALBUM) { 1724 if (!cloudAuthority.equals(suggestion.getAuthority())) { 1725 Log.d(TAG, "Album search suggestion does not belong to cloud provider. " 1726 + "Not syncing search results with cloud provider for request id " 1727 + searchRequestId); 1728 return false; 1729 } 1730 } 1731 } 1732 1733 Log.d(TAG, "Scheduling search results sync with cloud provider: " + searchRequestId); 1734 syncManager.syncSearchResultsForProvider( 1735 searchRequestId, 1736 SYNC_CLOUD_ONLY, 1737 cloudAuthority); 1738 return true; 1739 } 1740 1741 /** 1742 * @param searchRequestId Identifier of a search request. 1743 * @return A response bundle containing the search request id. 1744 */ 1745 @NonNull getSearchRequestInitResponse(int searchRequestId)1746 private static Bundle getSearchRequestInitResponse(int searchRequestId) { 1747 final Bundle response = new Bundle(); 1748 response.putInt(PickerSQLConstants.EXTRA_SEARCH_REQUEST_ID, searchRequestId); 1749 return response; 1750 } 1751 } 1752