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