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 com.android.providers.media.photopicker; 18 19 import static android.database.DatabaseUtils.dumpCursorToString; 20 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALL_PROJECTION; 21 import static android.provider.CloudMediaProviderContract.AlbumColumns.AUTHORITY; 22 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO; 23 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT; 24 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME; 25 import static android.provider.MediaStore.MY_UID; 26 27 import static com.android.providers.media.PickerUriResolver.getAlbumUri; 28 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri; 29 import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_ALBUM_SYNC_WORK_NAME; 30 import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_LOCAL_SYNC_WORK_NAME; 31 import static com.android.providers.media.photopicker.sync.WorkManagerInitializer.getWorkManager; 32 33 import static java.util.Objects.requireNonNull; 34 35 import android.content.Context; 36 import android.content.Intent; 37 import android.database.Cursor; 38 import android.database.CursorWrapper; 39 import android.database.MergeCursor; 40 import android.os.Bundle; 41 import android.os.Trace; 42 import android.provider.MediaStore; 43 import android.text.TextUtils; 44 import android.util.Log; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.internal.logging.InstanceId; 51 import com.android.providers.media.ConfigStore; 52 import com.android.providers.media.photopicker.data.CloudProviderQueryExtras; 53 import com.android.providers.media.photopicker.data.PickerDbFacade; 54 import com.android.providers.media.photopicker.data.PickerSyncRequestExtras; 55 import com.android.providers.media.photopicker.metrics.NonUiEventLogger; 56 import com.android.providers.media.photopicker.sync.PickerSyncManager; 57 import com.android.providers.media.photopicker.sync.SyncCompletionWaiter; 58 import com.android.providers.media.photopicker.sync.SyncTrackerRegistry; 59 import com.android.providers.media.util.ForegroundThread; 60 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Objects; 67 68 /** 69 * Fetches data for the picker UI from the db and cloud/local providers 70 */ 71 public class PickerDataLayer { 72 private static final String TAG = "PickerDataLayer"; 73 private static final boolean DEBUG = false; 74 private static final boolean DEBUG_DUMP_CURSORS = false; 75 private static final int CLOUD_SYNC_TIMEOUT_MILLIS = 500; 76 77 public static final String QUERY_ARG_LOCAL_ONLY = "android:query-arg-local-only"; 78 79 public static final String QUERY_DATE_TAKEN_BEFORE_MS = "android:query-date-taken-before-ms"; 80 81 public static final String QUERY_ID_SELECTION = "android:query-id-selection"; 82 public static final String QUERY_LOCAL_ID_SELECTION = "android:query-local-id-selection"; 83 public static final String QUERY_CLOUD_ID_SELECTION = "android:query-cloud-id-selection"; 84 // This should be used to indicate if the ids passed in the query arguments should be checked 85 // for permission and authority or not. This shall be used for pre-selection uris passed in 86 // picker db query operations. 87 public static final String QUERY_SHOULD_SCREEN_SELECTION_URIS = 88 "android:query-should-screen-selection-uris"; 89 public static final String QUERY_ROW_ID = "android:query-row-id"; 90 91 @NonNull 92 private final Context mContext; 93 @NonNull 94 private final PickerDbFacade mDbFacade; 95 @NonNull 96 private final PickerSyncController mSyncController; 97 @NonNull 98 private final PickerSyncManager mSyncManager; 99 @NonNull 100 private final String mLocalProvider; 101 @NonNull 102 private final ConfigStore mConfigStore; 103 104 @VisibleForTesting PickerDataLayer(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull PickerSyncController syncController, @NonNull ConfigStore configStore, @NonNull PickerSyncManager syncManager)105 public PickerDataLayer(@NonNull Context context, @NonNull PickerDbFacade dbFacade, 106 @NonNull PickerSyncController syncController, @NonNull ConfigStore configStore, 107 @NonNull PickerSyncManager syncManager) { 108 mContext = requireNonNull(context); 109 mDbFacade = requireNonNull(dbFacade); 110 mSyncController = requireNonNull(syncController); 111 mLocalProvider = requireNonNull(dbFacade.getLocalProvider()); 112 mConfigStore = requireNonNull(configStore); 113 mSyncManager = syncManager; 114 115 // Add a subscriber to config store changes to monitor the allowlist. 116 mConfigStore.addOnChangeListener( 117 ForegroundThread.getExecutor(), 118 this::validateCurrentCloudProviderOnAllowlistChange); 119 } 120 121 /** 122 * Create a new instance of PickerDataLayer. 123 */ create(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull PickerSyncController syncController, @NonNull ConfigStore configStore)124 public static PickerDataLayer create(@NonNull Context context, @NonNull PickerDbFacade dbFacade, 125 @NonNull PickerSyncController syncController, @NonNull ConfigStore configStore) { 126 PickerSyncManager syncManager = new PickerSyncManager( 127 getWorkManager(context), context, configStore, /* schedulePeriodicSyncs */ true); 128 return new PickerDataLayer(context, dbFacade, syncController, configStore, syncManager); 129 } 130 131 /** 132 * Returns {@link Cursor} with all local media part of the given album in {@code queryArgs} 133 */ fetchLocalMedia(Bundle queryArgs)134 public Cursor fetchLocalMedia(Bundle queryArgs) { 135 queryArgs.putBoolean(QUERY_ARG_LOCAL_ONLY, true); 136 return fetchMediaInternal(queryArgs); 137 } 138 139 /** 140 * Returns {@link Cursor} with all local+cloud media part of the given album in 141 * {@code queryArgs} 142 */ fetchAllMedia(Bundle queryArgs)143 public Cursor fetchAllMedia(Bundle queryArgs) { 144 queryArgs.putBoolean(QUERY_ARG_LOCAL_ONLY, false); 145 return fetchMediaInternal(queryArgs); 146 } 147 fetchMediaInternal(Bundle queryArgs)148 private Cursor fetchMediaInternal(Bundle queryArgs) { 149 if (DEBUG) { 150 Log.d(TAG, "fetchMediaInternal() " 151 + (queryArgs.getBoolean(QUERY_ARG_LOCAL_ONLY) ? "LOCAL_ONLY" : "ALL") 152 + " args=" + queryArgs); 153 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 154 } 155 156 final CloudProviderQueryExtras queryExtras = 157 CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs); 158 final String albumAuthority = queryExtras.getAlbumAuthority(); 159 160 Trace.beginSection(traceSectionName("fetchMediaInternal", albumAuthority)); 161 162 Cursor result = null; 163 try { 164 final boolean isLocalOnly = queryExtras.isLocalOnly(); 165 final String albumId = queryExtras.getAlbumId(); 166 // Use media table for all media except albums. Merged categories like, 167 // favorites and video are tagged in the media table and are not a part of 168 // album_media. 169 if (TextUtils.isEmpty(albumId) || queryExtras.isMergedAlbum()) { 170 // Refresh the 'media' table 171 if (shouldSyncBeforePickerQuery()) { 172 syncAllMedia(isLocalOnly); 173 } else { 174 // Wait for local sync to finish indefinitely 175 SyncCompletionWaiter.waitForSync( 176 getWorkManager(mContext), 177 SyncTrackerRegistry.getLocalSyncTracker(), 178 IMMEDIATE_LOCAL_SYNC_WORK_NAME); 179 Log.i(TAG, "Local sync is complete"); 180 181 // Wait for on cloud sync with timeout 182 if (!isLocalOnly) { 183 boolean syncIsComplete = SyncCompletionWaiter.waitForSyncWithTimeout( 184 SyncTrackerRegistry.getCloudSyncTracker(), 185 CLOUD_SYNC_TIMEOUT_MILLIS); 186 Log.i(TAG, "Finished waiting for cloud sync. Is cloud sync complete: " 187 + syncIsComplete); 188 } 189 } 190 191 // Fetch all merged and deduped cloud and local media from 'media' table 192 // This also matches 'merged' albums like Favorites because |authority| will 193 // be null, hence we have to fetch the data from the picker db 194 result = mDbFacade.queryMediaForUi(queryExtras.toQueryFilter()); 195 } else { 196 if (isLocalOnly && !isLocal(albumAuthority)) { 197 // This is error condition because when cloud content is disabled, we shouldn't 198 // send any cloud albums in available albums list. 199 throw new IllegalStateException( 200 "Can't exclude cloud contents in cloud album " + albumAuthority); 201 } 202 203 // The album type here can only be local or cloud because merged categories like, 204 // Favorites and Videos would hit the first condition. 205 // Refresh the 'album_media' table 206 if (shouldSyncBeforePickerQuery()) { 207 mSyncController.syncAlbumMedia(albumId, isLocal(albumAuthority)); 208 } else { 209 SyncCompletionWaiter.waitForSync( 210 getWorkManager(mContext), 211 SyncTrackerRegistry.getAlbumSyncTracker(isLocal(albumAuthority)), 212 IMMEDIATE_ALBUM_SYNC_WORK_NAME); 213 Log.i(TAG, "Album sync is complete"); 214 } 215 216 // Fetch album specific media for local or cloud from 'album_media' table 217 result = mDbFacade.queryAlbumMediaForUi( 218 queryExtras.toQueryFilter(), albumAuthority); 219 } 220 return result; 221 } finally { 222 Trace.endSection(); 223 if (DEBUG) { 224 if (result == null) { 225 Log.d(TAG, "fetchMediaInternal()'s result is null"); 226 } else { 227 Log.d(TAG, "fetchMediaInternal() loaded " + result.getCount() + " items"); 228 if (DEBUG_DUMP_CURSORS) { 229 Log.v(TAG, dumpCursorToString(result)); 230 } 231 } 232 } 233 } 234 } 235 syncAllMedia(boolean isLocalOnly)236 private void syncAllMedia(boolean isLocalOnly) { 237 if (isLocalOnly) { 238 mSyncController.syncAllMediaFromLocalProvider(/* cancellationSignal= */ null); 239 } else { 240 mSyncController.syncAllMedia(); 241 } 242 } 243 244 /** 245 * Returns {@link Cursor} with all local and merged albums with local items. 246 */ fetchLocalAlbums(Bundle queryArgs)247 public Cursor fetchLocalAlbums(Bundle queryArgs) { 248 queryArgs.putBoolean(QUERY_ARG_LOCAL_ONLY, true); 249 return fetchAlbumsInternal(queryArgs); 250 } 251 252 /** 253 * Returns {@link Cursor} with all local, merged and cloud albums 254 */ fetchAllAlbums(Bundle queryArgs)255 public Cursor fetchAllAlbums(Bundle queryArgs) { 256 queryArgs.putBoolean(QUERY_ARG_LOCAL_ONLY, false); 257 return fetchAlbumsInternal(queryArgs); 258 } 259 fetchAlbumsInternal(Bundle queryArgs)260 private Cursor fetchAlbumsInternal(Bundle queryArgs) { 261 if (DEBUG) { 262 Log.d(TAG, "fetchAlbums() " 263 + (queryArgs.getBoolean(QUERY_ARG_LOCAL_ONLY) ? "LOCAL_ONLY" : "ALL") 264 + " args=" + queryArgs); 265 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 266 } 267 268 Trace.beginSection(traceSectionName("fetchAlbums")); 269 270 Cursor result = null; 271 try { 272 final boolean isLocalOnly = queryArgs.getBoolean(QUERY_ARG_LOCAL_ONLY, false); 273 // Refresh the 'media' table so that 'merged' albums (Favorites and Videos) are 274 // up-to-date 275 if (shouldSyncBeforePickerQuery()) { 276 syncAllMedia(isLocalOnly); 277 } 278 279 final String cloudProvider = mSyncController.getCloudProvider(); 280 final CloudProviderQueryExtras queryExtras = 281 CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs); 282 final Bundle cloudMediaArgs = queryExtras.toCloudMediaBundle(); 283 final List<Cursor> cursors = new ArrayList<>(); 284 final Bundle cursorExtra = new Bundle(); 285 cursorExtra.putString(MediaStore.EXTRA_CLOUD_PROVIDER, cloudProvider); 286 cursorExtra.putString(MediaStore.EXTRA_LOCAL_PROVIDER, mLocalProvider); 287 288 // Favorites and Videos are merged albums. 289 final Cursor mergedAlbums = mDbFacade.getMergedAlbums(queryExtras.toQueryFilter(), 290 cloudProvider); 291 if (mergedAlbums != null) { 292 cursors.add(mergedAlbums); 293 } 294 295 final Cursor localAlbums = queryProviderAlbums(mLocalProvider, cloudMediaArgs); 296 if (localAlbums != null) { 297 cursors.add(new AlbumsCursorWrapper(localAlbums, mLocalProvider)); 298 } 299 300 if (!isLocalOnly) { 301 final Cursor cloudAlbums = queryProviderAlbums(cloudProvider, cloudMediaArgs); 302 if (cloudAlbums != null) { 303 // There's a bug in the Merge Cursor code (b/241096151) such that if the cursors 304 // being merged have different projections, the data gets corrupted post IPC. 305 // Fixing this bug requires a dessert release and will not be compatible with 306 // android T-. Hence, we're using {@link AlbumsCursorWrapper} that unifies the 307 // local and cloud album cursors' projections to {@link ALL_PROJECTION} 308 cursors.add(new AlbumsCursorWrapper(cloudAlbums, cloudProvider)); 309 } 310 } 311 312 if (cursors.isEmpty()) { 313 return null; 314 } 315 316 result = new MergeCursor(cursors.toArray(new Cursor[cursors.size()])); 317 result.setExtras(cursorExtra); 318 return result; 319 } finally { 320 Trace.endSection(); 321 if (DEBUG) { 322 if (result == null) { 323 Log.d(TAG, "fetchAlbumsInternal()'s result is null"); 324 } else { 325 Log.d(TAG, "fetchAlbumsInternal() loaded " + result.getCount() + " items"); 326 if (DEBUG_DUMP_CURSORS) { 327 Log.v(TAG, dumpCursorToString(result)); 328 } 329 } 330 } 331 } 332 } 333 334 @Nullable fetchCloudAccountInfo()335 public AccountInfo fetchCloudAccountInfo() { 336 if (DEBUG) { 337 Log.d(TAG, "fetchCloudAccountInfo()"); 338 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 339 } 340 341 final String cloudProvider = mDbFacade.getCloudProvider(); 342 if (cloudProvider == null) { 343 return null; 344 } 345 346 Trace.beginSection(traceSectionName("fetchCloudAccountInfo")); 347 try { 348 return fetchCloudAccountInfoInternal(cloudProvider); 349 } catch (Exception e) { 350 Log.w(TAG, "Failed to fetch account info from cloud provider: " + cloudProvider, e); 351 return null; 352 } finally { 353 Trace.endSection(); 354 } 355 } 356 357 @Nullable fetchCloudAccountInfoInternal(@onNull String cloudProvider)358 private AccountInfo fetchCloudAccountInfoInternal(@NonNull String cloudProvider) { 359 final Bundle accountBundle = mContext.getContentResolver() 360 .call(getMediaCollectionInfoUri(cloudProvider), METHOD_GET_MEDIA_COLLECTION_INFO, 361 /* arg */ null, /* extras */ new Bundle()); 362 if (accountBundle == null) { 363 Log.e(TAG, 364 "Media collection info received is null. Failed to fetch Cloud account " 365 + "information."); 366 return null; 367 } 368 final String accountName = accountBundle.getString(ACCOUNT_NAME); 369 if (accountName == null) { 370 return null; 371 } 372 final Intent configIntent = accountBundle.getParcelable(ACCOUNT_CONFIGURATION_INTENT); 373 374 return new AccountInfo(accountName, configIntent); 375 } 376 queryProviderAlbums(@ullable String authority, Bundle queryArgs)377 private Cursor queryProviderAlbums(@Nullable String authority, Bundle queryArgs) { 378 if (authority == null) { 379 // Can happen if there is no cloud provider 380 return null; 381 } 382 383 Trace.beginSection(traceSectionName("queryProviderAlbums", authority)); 384 try { 385 return queryProviderAlbumsInternal(authority, queryArgs); 386 } finally { 387 Trace.endSection(); 388 } 389 } 390 queryProviderAlbumsInternal(@onNull String authority, Bundle queryArgs)391 private Cursor queryProviderAlbumsInternal(@NonNull String authority, Bundle queryArgs) { 392 final InstanceId instanceId = NonUiEventLogger.generateInstanceId(); 393 int numberOfAlbumsFetched = -1; 394 NonUiEventLogger.logPickerGetAlbumsStart(instanceId, MY_UID, authority); 395 try { 396 final Cursor res = mContext.getContentResolver().query(getAlbumUri(authority), 397 /* projection */ null, queryArgs, /* cancellationSignal */ null); 398 if (res != null) { 399 numberOfAlbumsFetched = res.getCount(); 400 } 401 return res; 402 } catch (Exception e) { 403 Log.w(TAG, "Failed to fetch cloud albums for: " + authority, e); 404 return null; 405 } finally { 406 NonUiEventLogger.logPickerGetAlbumsEnd(instanceId, MY_UID, authority, 407 numberOfAlbumsFetched); 408 } 409 } 410 isLocal(String authority)411 private boolean isLocal(String authority) { 412 return mLocalProvider.equals(authority); 413 } 414 traceSectionName(@onNull String method)415 private String traceSectionName(@NonNull String method) { 416 return traceSectionName(method, null); 417 } 418 traceSectionName(@onNull String method, @Nullable String authority)419 private String traceSectionName(@NonNull String method, @Nullable String authority) { 420 final StringBuilder sb = new StringBuilder("PDL.") 421 .append(method); 422 if (authority != null) { 423 sb.append('[').append(isLocal(authority) ? "local" : "cloud").append(']'); 424 } 425 return sb.toString(); 426 } 427 428 /** 429 * Triggers a sync operation based on the parameters. 430 */ initMediaData(@onNull PickerSyncRequestExtras syncRequestExtras)431 public void initMediaData(@NonNull PickerSyncRequestExtras syncRequestExtras) { 432 if (syncRequestExtras.shouldSyncMediaData()) { 433 // Sync media data 434 Log.i(TAG, "Init data request for the main photo grid i.e. media data." 435 + " Should sync with local provider only: " 436 + syncRequestExtras.shouldSyncLocalOnlyData()); 437 438 mSyncManager.syncMediaImmediately(syncRequestExtras.shouldSyncLocalOnlyData()); 439 } else { 440 // Sync album media data 441 Log.i(TAG, String.format("Init data request for album content of: %s" 442 + " Should sync with local provider only: %b", 443 syncRequestExtras.getAlbumId(), 444 syncRequestExtras.shouldSyncLocalOnlyData())); 445 446 validateAlbumMediaSyncArgs(syncRequestExtras); 447 448 // We don't need to sync in case of merged albums 449 if (!syncRequestExtras.shouldSyncMergedAlbum()) { 450 mSyncManager.syncAlbumMediaForProviderImmediately( 451 syncRequestExtras.getAlbumId(), 452 syncRequestExtras.getAlbumAuthority(), 453 isLocal(syncRequestExtras.getAlbumAuthority())); 454 } 455 } 456 } 457 validateAlbumMediaSyncArgs(PickerSyncRequestExtras syncRequestExtras)458 private void validateAlbumMediaSyncArgs(PickerSyncRequestExtras syncRequestExtras) { 459 if (!syncRequestExtras.shouldSyncMediaData()) { 460 Objects.requireNonNull(syncRequestExtras.getAlbumId(), 461 "Album Id can't be null for an album sync request."); 462 Objects.requireNonNull(syncRequestExtras.getAlbumAuthority(), 463 "Album authority can't be null for an album sync request."); 464 } 465 if (!syncRequestExtras.shouldSyncMediaData() 466 && !syncRequestExtras.shouldSyncMergedAlbum() 467 && syncRequestExtras.shouldSyncLocalOnlyData() 468 && !isLocal(syncRequestExtras.getAlbumAuthority())) { 469 throw new IllegalStateException( 470 "Can't exclude cloud contents in cloud album " 471 + syncRequestExtras.getAlbumAuthority()); 472 } 473 } 474 475 476 /** 477 * Handles notification about media events like inserts/updates/deletes received from cloud or 478 * local providers. 479 * @param localOnly - whether the media event is coming from the local provider 480 */ handleMediaEventNotification(Boolean localOnly)481 public void handleMediaEventNotification(Boolean localOnly) { 482 try { 483 mSyncManager.syncMediaProactively(localOnly); 484 } catch (RuntimeException e) { 485 // Catch any unchecked exceptions so that critical paths in MP that call this method are 486 // not affected by Picker related issues. 487 Log.e(TAG, "Could not handle media event notification ", e); 488 } 489 } 490 491 public static class AccountInfo { 492 public final String accountName; 493 public final Intent accountConfigurationIntent; 494 AccountInfo(String accountName, Intent accountConfigurationIntent)495 public AccountInfo(String accountName, Intent accountConfigurationIntent) { 496 this.accountName = accountName; 497 this.accountConfigurationIntent = accountConfigurationIntent; 498 } 499 } 500 501 /** 502 * A {@link CursorWrapper} that exposes the data stored in the underlying {@link Cursor} in the 503 * {@link ALL_PROJECTION} "format", additionally overriding the {@link AUTHORITY} column. 504 * Columns from the underlying that are not in the {@link ALL_PROJECTION} are ignored. 505 * Missing columns (except {@link AUTHORITY}) are set with default value of {@code null}. 506 */ 507 private static class AlbumsCursorWrapper extends CursorWrapper { 508 static final String TAG = "AlbumsCursorWrapper"; 509 510 @NonNull static final Map<String, Integer> COLUMN_NAME_TO_INDEX_MAP; 511 static final int AUTHORITY_COLUMN_INDEX; 512 513 static { 514 final Map<String, Integer> map = new HashMap<>(); 515 for (int columnIndex = 0; columnIndex < ALL_PROJECTION.length; columnIndex++) { map.put(ALL_PROJECTION[columnIndex], columnIndex)516 map.put(ALL_PROJECTION[columnIndex], columnIndex); 517 } 518 COLUMN_NAME_TO_INDEX_MAP = map; 519 AUTHORITY_COLUMN_INDEX = map.get(AUTHORITY); 520 } 521 522 @NonNull final String mAuthority; 523 @NonNull final int[] mColumnIndexToCursorColumnIndexArray; 524 525 boolean mAuthorityMismatchLogged = false; 526 AlbumsCursorWrapper(@onNull Cursor cursor, @NonNull String authority)527 AlbumsCursorWrapper(@NonNull Cursor cursor, @NonNull String authority) { 528 super(requireNonNull(cursor)); 529 mAuthority = requireNonNull(authority); 530 531 mColumnIndexToCursorColumnIndexArray = new int[ALL_PROJECTION.length]; 532 for (int columnIndex = 0; columnIndex < ALL_PROJECTION.length; columnIndex++) { 533 final String columnName = ALL_PROJECTION[columnIndex]; 534 final int cursorColumnIndex = cursor.getColumnIndex(columnName); 535 mColumnIndexToCursorColumnIndexArray[columnIndex] = cursorColumnIndex; 536 } 537 } 538 539 @Override getColumnCount()540 public int getColumnCount() { 541 return ALL_PROJECTION.length; 542 } 543 544 @Override getColumnIndex(String columnName)545 public int getColumnIndex(String columnName) { 546 return COLUMN_NAME_TO_INDEX_MAP.get(columnName); 547 } 548 549 @Override getColumnIndexOrThrow(String columnName)550 public int getColumnIndexOrThrow(String columnName) 551 throws IllegalArgumentException { 552 final int columnIndex = getColumnIndex(columnName); 553 if (columnIndex < 0) { 554 throw new IllegalArgumentException("column '" + columnName 555 + "' does not exist. Available columns: " 556 + Arrays.toString(getColumnNames())); 557 } 558 return columnIndex; 559 } 560 561 @Override getColumnName(int columnIndex)562 public String getColumnName(int columnIndex) { 563 return ALL_PROJECTION[columnIndex]; 564 } 565 566 @Override getColumnNames()567 public String[] getColumnNames() { 568 return ALL_PROJECTION; 569 } 570 571 @Override getString(int columnIndex)572 public String getString(int columnIndex) { 573 // 1. Get value from the underlying cursor. 574 final int cursorColumnIndex = mColumnIndexToCursorColumnIndexArray[columnIndex]; 575 final String cursorValue = cursorColumnIndex != -1 576 ? getWrappedCursor().getString(cursorColumnIndex) : null; 577 578 // 2a. If this is NOT the AUTHORITY column: just return the value. 579 if (columnIndex != AUTHORITY_COLUMN_INDEX) { 580 return cursorValue; 581 } 582 583 // Validity check: the cursor's authority value, if present, is expected to match the 584 // mAuthority. Don't throw though, just log (at WARN). Also, only log once for the 585 // cursor (we don't need 10,000 of these lines in the log). 586 if (!mAuthorityMismatchLogged 587 && cursorValue != null && !cursorValue.equals(mAuthority)) { 588 Log.w(TAG, "Cursor authority - '" + cursorValue + "' - is different from the " 589 + "expected authority '" + mAuthority + "'"); 590 mAuthorityMismatchLogged = true; 591 } 592 593 // 2b. If this IS the AUTHORITY column: "override" whatever value (which may be null) 594 // is stored in the cursor. 595 return mAuthority; 596 } 597 598 @Override getType(int columnIndex)599 public int getType(int columnIndex) { 600 // 1. Get value from the underlying cursor. 601 final int cursorColumnIndex = mColumnIndexToCursorColumnIndexArray[columnIndex]; 602 final int cursorValue = cursorColumnIndex != -1 603 ? getWrappedCursor().getType(cursorColumnIndex) : Cursor.FIELD_TYPE_NULL; 604 605 // 2a. If this is NOT the AUTHORITY column: just return the value. 606 if (columnIndex != AUTHORITY_COLUMN_INDEX) { 607 return cursorValue; 608 } 609 610 // 2b. If this IS the AUTHORITY column: "override" whatever value (which may be 0) 611 // is stored in the cursor. 612 return Cursor.FIELD_TYPE_STRING; 613 } 614 } 615 616 /** 617 * For cloud feature enabled scenarios, sync request is sent from the 618 * MediaStore.PICKER_MEDIA_INIT_CALL method call once when a fresh grid needs to be filled 619 * populated data. This is because UI paginated queries are supported when cloud feature 620 * enabled. This avoids triggering a sync for the same dataset for each paged query received 621 * from the UI. 622 */ shouldSyncBeforePickerQuery()623 private boolean shouldSyncBeforePickerQuery() { 624 return !mConfigStore.isCloudMediaInPhotoPickerEnabled(); 625 } 626 627 /** 628 * Checks the current allowed list of Cloud Provider packages, and ensures that the currently 629 * set provider is a member of the allowlist. In the event the current Cloud Provider is not on 630 * the list, the current Cloud Provider is removed. 631 */ validateCurrentCloudProviderOnAllowlistChange()632 private void validateCurrentCloudProviderOnAllowlistChange() { 633 634 List<String> currentAllowlist = mConfigStore.getAllowedCloudProviderPackages(); 635 String currentCloudProvider = mSyncController.getCurrentCloudProviderInfo().packageName; 636 637 if (!currentAllowlist.contains(currentCloudProvider)) { 638 Log.d( 639 TAG, 640 String.format( 641 "Cloud provider allowlist was changed, and the current cloud provider" 642 + " is no longer on the allowlist." 643 + " Allowlist: %s" 644 + " Current Provider: %s", 645 currentAllowlist.toString(), currentCloudProvider)); 646 mSyncController.notifyPackageRemoval(currentCloudProvider); 647 } 648 } 649 } 650