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.content.ContentResolver.EXTRA_HONORED_ARGS; 20 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID; 21 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID; 22 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE; 23 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN; 24 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION; 25 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT; 26 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME; 27 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION; 28 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID; 29 import static android.provider.MediaStore.AUTHORITY; 30 import static android.provider.MediaStore.MY_UID; 31 import static android.provider.MediaStore.PER_USER_RANGE; 32 33 import static com.android.providers.media.PickerUriResolver.INIT_PATH; 34 import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI; 35 import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI; 36 import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri; 37 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri; 38 import static com.android.providers.media.PickerUriResolver.getMediaUri; 39 import static com.android.providers.media.photopicker.NotificationContentObserver.ALBUM_CONTENT; 40 import static com.android.providers.media.photopicker.NotificationContentObserver.MEDIA; 41 import static com.android.providers.media.photopicker.NotificationContentObserver.UPDATE; 42 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString; 43 44 import android.annotation.IntDef; 45 import android.content.ContentProviderClient; 46 import android.content.ContentResolver; 47 import android.content.Context; 48 import android.content.Intent; 49 import android.content.SharedPreferences; 50 import android.content.pm.PackageManager; 51 import android.database.Cursor; 52 import android.database.SQLException; 53 import android.net.Uri; 54 import android.os.Bundle; 55 import android.os.CancellationSignal; 56 import android.os.Handler; 57 import android.os.RemoteException; 58 import android.os.Trace; 59 import android.os.storage.StorageManager; 60 import android.provider.CloudMediaProvider; 61 import android.provider.CloudMediaProviderContract; 62 import android.provider.CloudMediaProviderContract.MediaColumns; 63 import android.text.TextUtils; 64 import android.util.ArraySet; 65 import android.util.Log; 66 67 import androidx.annotation.NonNull; 68 import androidx.annotation.Nullable; 69 import androidx.annotation.VisibleForTesting; 70 71 import com.android.internal.logging.InstanceId; 72 import com.android.modules.utils.BackgroundThread; 73 import com.android.modules.utils.build.SdkLevel; 74 import com.android.providers.media.ConfigStore; 75 import com.android.providers.media.R; 76 import com.android.providers.media.photopicker.data.CloudProviderInfo; 77 import com.android.providers.media.photopicker.data.PickerDbFacade; 78 import com.android.providers.media.photopicker.metrics.NonUiEventLogger; 79 import com.android.providers.media.photopicker.sync.CloseableReentrantLock; 80 import com.android.providers.media.photopicker.sync.PickerSyncLockManager; 81 import com.android.providers.media.photopicker.util.CloudProviderUtils; 82 import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException; 83 import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException; 84 import com.android.providers.media.photopicker.util.exceptions.WorkCancelledException; 85 import com.android.providers.media.photopicker.v2.PickerNotificationSender; 86 import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo; 87 88 import java.io.PrintWriter; 89 import java.lang.annotation.Retention; 90 import java.lang.annotation.RetentionPolicy; 91 import java.util.ArrayList; 92 import java.util.Arrays; 93 import java.util.List; 94 import java.util.Objects; 95 import java.util.Set; 96 import java.util.concurrent.TimeUnit; 97 import java.util.concurrent.locks.ReentrantLock; 98 99 /** 100 * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances on the device 101 * into the picker db. 102 */ 103 public class PickerSyncController { 104 105 public static final ReentrantLock sIdleMaintenanceSyncLock = new ReentrantLock(); 106 private static final String TAG = "PickerSyncController"; 107 private static final boolean DEBUG = false; 108 109 private static final String PREFS_KEY_CLOUD_PROVIDER_AUTHORITY = "cloud_provider_authority"; 110 private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:"; 111 private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:"; 112 113 private static final String PREFS_KEY_RESUME = "resume"; 114 private static final String PREFS_KEY_OPERATION_MEDIA_ADD_PREFIX = "media_add:"; 115 private static final String PREFS_KEY_OPERATION_MEDIA_REMOVE_PREFIX = "media_remove:"; 116 private static final String PREFS_KEY_OPERATION_ALBUM_ADD_PREFIX = "album_add:"; 117 118 private static final String PICKER_USER_PREFS_FILE_NAME = "picker_user_prefs"; 119 public static final String PICKER_SYNC_PREFS_FILE_NAME = "picker_sync_prefs"; 120 public static final String LOCAL_PICKER_PROVIDER_AUTHORITY = 121 "com.android.providers.media.photopicker"; 122 123 private static final String PREFS_VALUE_CLOUD_PROVIDER_UNSET = "-"; 124 125 private static final int OPERATION_ADD_MEDIA = 1; 126 private static final int OPERATION_ADD_ALBUM = 2; 127 private static final int OPERATION_REMOVE_MEDIA = 3; 128 129 @IntDef( 130 flag = false, 131 value = {OPERATION_ADD_MEDIA, OPERATION_ADD_ALBUM, OPERATION_REMOVE_MEDIA}) 132 @Retention(RetentionPolicy.SOURCE) 133 private @interface OperationType {} 134 135 private static final int SYNC_TYPE_NONE = 0; 136 private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1; 137 private static final int SYNC_TYPE_MEDIA_FULL = 2; 138 private static final int SYNC_TYPE_MEDIA_RESET = 3; 139 private static final int SYNC_TYPE_MEDIA_FULL_WITH_RESET = 4; 140 public static final int PAGE_SIZE = 1000; 141 @NonNull 142 private static final Handler sBgThreadHandler = BackgroundThread.getHandler(); 143 @IntDef(flag = false, prefix = { "SYNC_TYPE_" }, value = { 144 SYNC_TYPE_NONE, 145 SYNC_TYPE_MEDIA_INCREMENTAL, 146 SYNC_TYPE_MEDIA_FULL, 147 SYNC_TYPE_MEDIA_RESET, 148 SYNC_TYPE_MEDIA_FULL_WITH_RESET, 149 }) 150 @Retention(RetentionPolicy.SOURCE) 151 private @interface SyncType {} 152 153 private static final long DEFAULT_GENERATION = -1; 154 private final Context mContext; 155 private final ConfigStore mConfigStore; 156 private final PickerDbFacade mDbFacade; 157 private final SharedPreferences mSyncPrefs; 158 private final SharedPreferences mUserPrefs; 159 private final PickerSyncLockManager mPickerSyncLockManager; 160 private final String mLocalProvider; 161 162 private CloudProviderInfo mCloudProviderInfo; 163 @NonNull 164 private ProviderCollectionInfo mLatestLocalProviderCollectionInfo; 165 @NonNull 166 private ProviderCollectionInfo mLatestCloudProviderCollectionInfo; 167 @NonNull 168 private SearchState mSearchState; 169 @NonNull 170 private CategoriesState mCategoriesState; 171 @Nullable 172 private static PickerSyncController sInstance; 173 174 /** 175 * This URI path when used in a MediaProvider.query() method redirects the call to media_grants 176 * table present in the external database. 177 */ 178 private static final String MEDIA_GRANTS_URI_PATH = "content://media/media_grants"; 179 180 /** 181 * Extra that can be passed in the grants sync query to ensure only the data corresponding to 182 * the required mimeTypes is synced. 183 */ 184 public static final String EXTRA_MEDIA_GRANTS_MIME_TYPES = "media_grant_mime_type_selection"; 185 186 /** 187 * Initialize {@link PickerSyncController} object.{@link PickerSyncController} should only be 188 * initialized from {@link com.android.providers.media.MediaProvider#onCreate}. 189 * 190 * @param context the app context of type {@link Context} 191 * @param dbFacade instance of {@link PickerDbFacade} that will be used for DB queries. 192 * @param configStore {@link ConfigStore} that returns the sync config of the device. 193 * @return an instance of {@link PickerSyncController} 194 */ 195 @NonNull initialize(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull PickerSyncLockManager pickerSyncLockManager)196 public static PickerSyncController initialize(@NonNull Context context, 197 @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull 198 PickerSyncLockManager pickerSyncLockManager) { 199 return initialize(context, dbFacade, configStore, pickerSyncLockManager, 200 LOCAL_PICKER_PROVIDER_AUTHORITY); 201 } 202 203 /** 204 * Initialize {@link PickerSyncController} object.{@link PickerSyncController} should only be 205 * initialized from {@link com.android.providers.media.MediaProvider#onCreate}. 206 * 207 * @param context the app context of type {@link Context} 208 * @param dbFacade instance of {@link PickerDbFacade} that will be used for DB queries. 209 * @param configStore {@link ConfigStore} that returns the sync config of the device. 210 * @param localProvider is the name of the local provider that is responsible for providing the 211 * local media items. 212 * @return an instance of {@link PickerSyncController} 213 */ 214 @NonNull 215 @VisibleForTesting initialize(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull PickerSyncLockManager pickerSyncLockManager, @NonNull String localProvider)216 public static PickerSyncController initialize(@NonNull Context context, 217 @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, 218 @NonNull PickerSyncLockManager pickerSyncLockManager, @NonNull String localProvider) { 219 sInstance = new PickerSyncController(context, dbFacade, configStore, pickerSyncLockManager, 220 localProvider); 221 return sInstance; 222 } 223 224 /** 225 * This method is available for injecting a mock instance from tests. PickerSyncController is 226 * used in Worker classes. They cannot directly be injected with a mock controller instance. 227 */ 228 @VisibleForTesting(otherwise = VisibleForTesting.NONE) setInstance(PickerSyncController controller)229 public static void setInstance(PickerSyncController controller) { 230 sInstance = controller; 231 } 232 233 /** 234 * Returns PickerSyncController instance if it is initialized else throws an exception. 235 * @return a PickerSyncController object. 236 * @throws IllegalStateException when the PickerSyncController is not initialized. 237 */ 238 @NonNull getInstanceOrThrow()239 public static PickerSyncController getInstanceOrThrow() throws IllegalStateException { 240 if (sInstance == null) { 241 throw new IllegalStateException("PickerSyncController is not initialised."); 242 } 243 return sInstance; 244 } 245 PickerSyncController(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull PickerSyncLockManager pickerSyncLockManager, @NonNull String localProvider)246 private PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade, 247 @NonNull ConfigStore configStore, @NonNull PickerSyncLockManager pickerSyncLockManager, 248 @NonNull String localProvider) { 249 mContext = context; 250 mConfigStore = configStore; 251 mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME, 252 Context.MODE_PRIVATE); 253 mUserPrefs = mContext.getSharedPreferences(PICKER_USER_PREFS_FILE_NAME, 254 Context.MODE_PRIVATE); 255 mDbFacade = dbFacade; 256 mPickerSyncLockManager = pickerSyncLockManager; 257 mLocalProvider = localProvider; 258 mSearchState = new SearchState(mConfigStore); 259 mCategoriesState = new CategoriesState(mConfigStore); 260 261 // Listen to the device config, and try to enable cloud features when the config changes. 262 mConfigStore.addOnChangeListener(BackgroundThread.getExecutor(), this::initCloudProvider); 263 initCloudProvider(); 264 } 265 266 /** 267 * This method is called after the broadcast intent action {@link Intent.ACTION_BOOT_COMPLETE} 268 * is received. 269 */ onBootComplete()270 public void onBootComplete() { 271 tryEnablingCloudMediaQueries(/* delay */ TimeUnit.MINUTES.toMillis(3)); 272 } 273 274 private Integer mEnableCloudQueryRemainingRetry = 2; 275 276 /** 277 * Attempt to enable cloud media queries in Picker DB with a retry mechanism. 278 */ 279 @VisibleForTesting tryEnablingCloudMediaQueries(@onNull long delay)280 public void tryEnablingCloudMediaQueries(@NonNull long delay) { 281 Log.d(TAG, "Schedule enable cloud media query task."); 282 283 BackgroundThread.getHandler().postDelayed(() -> { 284 Log.d(TAG, "Attempting to enable cloud media queries."); 285 try { 286 maybeEnableCloudMediaQueries(); 287 } catch (UnableToAcquireLockException | RequestObsoleteException | RuntimeException e) { 288 // Cloud media provider can return unexpected values if it's still bootstrapping. 289 // Retry in case a possibly transient error is encountered. 290 Log.d(TAG, "Error occurred, remaining retry count: " 291 + mEnableCloudQueryRemainingRetry, e); 292 mEnableCloudQueryRemainingRetry--; 293 if (mEnableCloudQueryRemainingRetry >= 0) { 294 tryEnablingCloudMediaQueries(/* delay */ TimeUnit.MINUTES.toMillis(3)); 295 } 296 } 297 }, delay); 298 } 299 300 @NonNull getPickerSyncLockManager()301 public PickerSyncLockManager getPickerSyncLockManager() { 302 return mPickerSyncLockManager; 303 } 304 initCloudProvider()305 private void initCloudProvider() { 306 try (CloseableReentrantLock ignored = mPickerSyncLockManager 307 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 308 if (!mConfigStore.isCloudMediaInPhotoPickerEnabled()) { 309 Log.d(TAG, "Cloud-Media-in-Photo-Picker feature is disabled during " + TAG 310 + " construction."); 311 persistCloudProviderInfo(CloudProviderInfo.EMPTY, /* shouldUnset */ false); 312 return; 313 } 314 315 final String cachedAuthority = mUserPrefs.getString( 316 PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, null); 317 318 if (isCloudProviderUnset(cachedAuthority)) { 319 Log.d(TAG, "Cloud provider state is unset during " + TAG + " construction."); 320 setCurrentCloudProviderInfo(CloudProviderInfo.EMPTY); 321 return; 322 } 323 324 initCloudProviderLocked(cachedAuthority); 325 } 326 } 327 initCloudProviderLocked(@ullable String cachedAuthority)328 private void initCloudProviderLocked(@Nullable String cachedAuthority) { 329 final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority); 330 331 if (Objects.equals(defaultInfo.authority, cachedAuthority)) { 332 // Just set it without persisting since it's not changing and persisting would 333 // notify the user that cloud media is now available 334 setCurrentCloudProviderInfo(defaultInfo); 335 } else { 336 // Persist it so that we notify the user that cloud media is now available 337 persistCloudProviderInfo(defaultInfo, /* shouldUnset */ false); 338 } 339 340 Log.d(TAG, "Initialized cloud provider to: " + defaultInfo.authority); 341 } 342 343 /** 344 * Enables Cloud media queries if the Picker DB is in sync with the latest collection id. 345 * @throws RequestObsoleteException if the cloud authority changes during the operation. 346 * @throws UnableToAcquireLockException If the required locks cannot be acquired to complete the 347 * operation. 348 */ maybeEnableCloudMediaQueries()349 public void maybeEnableCloudMediaQueries() 350 throws RequestObsoleteException, UnableToAcquireLockException { 351 try (CloseableReentrantLock ignored = 352 mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) { 353 final String cloudProvider = getCloudProviderWithTimeout(); 354 final SyncRequestParams params = 355 getSyncRequestParams(cloudProvider, /* isLocal */ false); 356 switch (params.syncType) { 357 case SYNC_TYPE_NONE: 358 case SYNC_TYPE_MEDIA_INCREMENTAL: 359 case SYNC_TYPE_MEDIA_FULL: 360 enablePickerCloudMediaQueries(cloudProvider, /* isLocal */ false); 361 break; 362 363 case SYNC_TYPE_MEDIA_RESET: 364 case SYNC_TYPE_MEDIA_FULL_WITH_RESET: 365 disablePickerCloudMediaQueries(/* isLocal */ false); 366 break; 367 368 default: 369 throw new IllegalArgumentException( 370 "Could not recognize sync type " + params.syncType); 371 } 372 } catch (IllegalArgumentException e) { 373 Log.e(TAG, "Could not enable picker cloud media queries", e); 374 } 375 } 376 377 /** 378 * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances 379 */ syncAllMedia()380 public void syncAllMedia() { 381 Log.d(TAG, "syncAllMedia"); 382 383 Trace.beginSection(traceSectionName("syncAllMedia")); 384 try { 385 syncAllMediaFromLocalProvider(/*CancellationSignal=*/ null); 386 syncAllMediaFromCloudProvider(/*CancellationSignal=*/ null); 387 } finally { 388 Trace.endSection(); 389 } 390 } 391 392 /** 393 * Syncs the local media 394 */ syncAllMediaFromLocalProvider(@ullable CancellationSignal cancellationSignal)395 public void syncAllMediaFromLocalProvider(@Nullable CancellationSignal cancellationSignal) { 396 // Picker sync and special format update can execute concurrently and run into a deadlock. 397 // Acquiring a lock before execution of each flow to avoid this. 398 sIdleMaintenanceSyncLock.lock(); 399 try { 400 final InstanceId instanceId = NonUiEventLogger.generateInstanceId(); 401 syncAllMediaFromProvider(mLocalProvider, /* isLocal */ true, /* retryOnFailure */ true, 402 /* enablePagedSync= */ true, instanceId, cancellationSignal); 403 } finally { 404 sIdleMaintenanceSyncLock.unlock(); 405 } 406 } 407 408 /** 409 * Syncs the cloud media 410 */ syncAllMediaFromCloudProvider(@ullable CancellationSignal cancellationSignal)411 public void syncAllMediaFromCloudProvider(@Nullable CancellationSignal cancellationSignal) { 412 413 try (CloseableReentrantLock ignored = 414 mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) { 415 final String cloudProvider = getCloudProviderWithTimeout(); 416 417 // Trigger a sync. 418 final InstanceId instanceId = NonUiEventLogger.generateInstanceId(); 419 final boolean didSyncFinish = syncAllMediaFromProvider(cloudProvider, 420 /* isLocal= */ false, /* retryOnFailure= */ true, /* enablePagedSync= */ true, 421 instanceId, cancellationSignal); 422 423 // Check if sync was completed successfully. 424 if (!didSyncFinish) { 425 Log.e(TAG, "Failed to fully complete sync with cloud provider - " + cloudProvider 426 + ". The cloud provider may have changed during the sync, or only a" 427 + " partial sync was completed."); 428 } 429 } catch (UnableToAcquireLockException e) { 430 Log.e(TAG, "Could not sync with the cloud provider", e); 431 } 432 } 433 434 /** 435 * Syncs album media from the local and currently enabled cloud {@link CloudMediaProvider} 436 * instances 437 */ syncAlbumMedia(String albumId, boolean isLocal)438 public void syncAlbumMedia(String albumId, boolean isLocal) { 439 if (isLocal) { 440 executeSyncAlbumReset(getLocalProvider(), isLocal, albumId); 441 syncAlbumMediaFromLocalProvider(albumId, /* cancellationSignal=*/ null); 442 } else { 443 try (CloseableReentrantLock ignored = mPickerSyncLockManager 444 .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) { 445 executeSyncAlbumReset(getCloudProviderWithTimeout(), isLocal, albumId); 446 } catch (UnableToAcquireLockException e) { 447 Log.e(TAG, "Unable to reset cloud album media " + albumId, e); 448 // Continue to attempt cloud album sync. This may show deleted album media on 449 // the album view. 450 } 451 syncAlbumMediaFromCloudProvider(albumId, /*cancellationSignal=*/ null); 452 } 453 } 454 455 /** Syncs album media from the local provider. */ syncAlbumMediaFromLocalProvider( @onNull String albumId, @Nullable CancellationSignal cancellationSignal)456 public void syncAlbumMediaFromLocalProvider( 457 @NonNull String albumId, @Nullable CancellationSignal cancellationSignal) { 458 syncAlbumMediaFromProvider(mLocalProvider, /* isLocal */ true, albumId, 459 /* enablePagedSync= */ true, cancellationSignal); 460 } 461 462 /** Syncs album media from the currently enabled cloud {@link CloudMediaProvider}. */ syncAlbumMediaFromCloudProvider( @onNull String albumId, @Nullable CancellationSignal cancellationSignal)463 public void syncAlbumMediaFromCloudProvider( 464 @NonNull String albumId, @Nullable CancellationSignal cancellationSignal) { 465 try (CloseableReentrantLock ignored = mPickerSyncLockManager 466 .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) { 467 syncAlbumMediaFromProvider(getCloudProviderWithTimeout(), /* isLocal */ false, albumId, 468 /* enablePagedSync= */ true, cancellationSignal); 469 } catch (UnableToAcquireLockException e) { 470 Log.e(TAG, "Unable to sync cloud album media " + albumId, e); 471 } 472 } 473 474 /** 475 * Resets media library previously synced from the current {@link CloudMediaProvider} as well 476 * as the {@link #mLocalProvider local provider}. 477 */ resetAllMedia()478 public void resetAllMedia() throws UnableToAcquireLockException { 479 // No need to acquire cloud lock for local reset. 480 resetAllMedia(mLocalProvider, /* isLocal */ true); 481 482 try (CloseableReentrantLock ignored = mPickerSyncLockManager 483 .lock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) { 484 485 // This does not fall in any sync path. Try to acquire the lock indefinitely. 486 resetAllMedia(getCloudProvider(), /* isLocal */ false); 487 } 488 } 489 resetAllMedia(@ullable String authority, boolean isLocal)490 private boolean resetAllMedia(@Nullable String authority, boolean isLocal) 491 throws UnableToAcquireLockException { 492 Trace.beginSection(traceSectionName("resetAllMedia", isLocal)); 493 try { 494 executeSyncReset(authority, isLocal); 495 return resetCachedMediaCollectionInfo(authority, isLocal); 496 } finally { 497 Trace.endSection(); 498 } 499 } 500 501 @NonNull getCloudProviderInfo(String authority, boolean ignoreAllowlist)502 private CloudProviderInfo getCloudProviderInfo(String authority, boolean ignoreAllowlist) { 503 if (authority == null) { 504 return CloudProviderInfo.EMPTY; 505 } 506 507 final List<CloudProviderInfo> availableProviders = ignoreAllowlist 508 ? CloudProviderUtils.getAllAvailableCloudProviders(mContext, mConfigStore) 509 : CloudProviderUtils.getAvailableCloudProviders(mContext, mConfigStore); 510 511 for (CloudProviderInfo info : availableProviders) { 512 if (Objects.equals(info.authority, authority)) { 513 return info; 514 } 515 } 516 517 return CloudProviderInfo.EMPTY; 518 } 519 520 /** 521 * @return list of available <b>and</b> allowlisted {@link CloudMediaProvider}-s. 522 */ 523 @VisibleForTesting getAvailableCloudProviders()524 List<CloudProviderInfo> getAvailableCloudProviders() { 525 return CloudProviderUtils.getAvailableCloudProviders(mContext, mConfigStore); 526 } 527 528 /** 529 * Enables a provider with {@code authority} as the default cloud {@link CloudMediaProvider}. 530 * If {@code authority} is set to {@code null}, it simply clears the cloud provider. 531 * 532 * Note, that this doesn't sync the new provider after switching, however, no cloud items will 533 * be available from the picker db until the next sync. Callers should schedule a sync in the 534 * background after switching providers. 535 * 536 * @return {@code true} if the provider was successfully enabled or cleared, {@code false} 537 * otherwise. 538 */ setCloudProvider(@ullable String authority)539 public boolean setCloudProvider(@Nullable String authority) { 540 Trace.beginSection(traceSectionName("setCloudProvider")); 541 try { 542 return setCloudProviderInternal(authority, /* ignoreAllowlist */ false); 543 } finally { 544 Trace.endSection(); 545 } 546 } 547 548 /** 549 * Set cloud provider ignoring allowlist. 550 * 551 * @return {@code true} if the provider was successfully enabled or cleared, {@code false} 552 * otherwise. 553 */ forceSetCloudProvider(@ullable String authority)554 public boolean forceSetCloudProvider(@Nullable String authority) { 555 Trace.beginSection(traceSectionName("forceSetCloudProvider")); 556 try { 557 return setCloudProviderInternal(authority, /* ignoreAllowlist */ true); 558 } finally { 559 Trace.endSection(); 560 } 561 } 562 setCloudProviderInternal(@ullable String authority, boolean ignoreAllowList)563 private boolean setCloudProviderInternal(@Nullable String authority, boolean ignoreAllowList) { 564 Log.d(TAG, "setCloudProviderInternal() auth=" + authority + ", " 565 + "ignoreAllowList=" + ignoreAllowList); 566 if (DEBUG) { 567 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 568 } 569 570 try (CloseableReentrantLock ignored = mPickerSyncLockManager 571 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 572 if (Objects.equals(mCloudProviderInfo.authority, authority)) { 573 Log.w(TAG, "Cloud provider already set: " + authority); 574 return true; 575 } 576 } 577 578 final CloudProviderInfo newProviderInfo = getCloudProviderInfo(authority, ignoreAllowList); 579 if (authority == null || !newProviderInfo.isEmpty()) { 580 try (CloseableReentrantLock ignored = mPickerSyncLockManager 581 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 582 // Disable cloud provider queries on the db until next sync 583 // This will temporarily *clear* the cloud provider on the db facade and prevent 584 // any queries from seeing cloud media until a sync where the cloud provider will be 585 // reset on the facade 586 mDbFacade.setCloudProvider(null); 587 588 final String oldAuthority = mCloudProviderInfo.authority; 589 persistCloudProviderInfo(newProviderInfo, /* shouldUnset */ true); 590 591 // TODO(b/242897322): Log from PickerViewModel using its InstanceId when relevant 592 NonUiEventLogger.logPickerCloudProviderChanged(newProviderInfo.uid, 593 newProviderInfo.packageName); 594 Log.i(TAG, "Cloud provider changed successfully. Old: " 595 + oldAuthority + ". New: " + newProviderInfo.authority); 596 } 597 598 return true; 599 } 600 601 Log.w(TAG, "Cloud provider not supported: " + authority); 602 return false; 603 } 604 605 /** 606 * @return {@link CloudProviderInfo} for the current {@link CloudMediaProvider} or 607 * {@link CloudProviderInfo#EMPTY} if the {@link CloudMediaProvider} integration is not 608 * enabled. 609 */ 610 @NonNull getCurrentCloudProviderInfo()611 public CloudProviderInfo getCurrentCloudProviderInfo() { 612 try (CloseableReentrantLock ignored = mPickerSyncLockManager 613 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 614 return mCloudProviderInfo; 615 } 616 } 617 618 /** 619 * Set {@link PickerSyncController#mCloudProviderInfo} as the current {@link CloudMediaProvider} 620 * or {@link CloudProviderInfo#EMPTY} if the {@link CloudMediaProvider} integration 621 * disabled by the user. 622 */ setCurrentCloudProviderInfo(@onNull CloudProviderInfo cloudProviderInfo)623 private void setCurrentCloudProviderInfo(@NonNull CloudProviderInfo cloudProviderInfo) { 624 try (CloseableReentrantLock ignored = mPickerSyncLockManager 625 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 626 mCloudProviderInfo = cloudProviderInfo; 627 } 628 } 629 630 /** 631 * This should not be used in picker sync paths because we should not wait on a lock 632 * indefinitely during the picker sync process. 633 * Use {@link this#getCloudProviderWithTimeout()} instead. 634 * @return {@link android.content.pm.ProviderInfo#authority authority} of the current 635 * {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider} 636 * integration is not enabled. 637 */ 638 @Nullable getCloudProvider()639 public String getCloudProvider() { 640 try (CloseableReentrantLock ignored = mPickerSyncLockManager 641 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 642 return mCloudProviderInfo.authority; 643 } 644 } 645 646 /** 647 * @return {@link android.content.pm.ProviderInfo#authority authority} of the current 648 * {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider} 649 * integration is not enabled. This operation acquires a lock internally with a timeout. 650 * @throws UnableToAcquireLockException if the lock was not acquired within the given timeout. 651 */ 652 @Nullable getCloudProviderWithTimeout()653 public String getCloudProviderWithTimeout() throws UnableToAcquireLockException { 654 try (CloseableReentrantLock ignored = mPickerSyncLockManager 655 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 656 return mCloudProviderInfo.authority; 657 } 658 } 659 660 /** 661 * @param defaultValue The default cloud provider authority to return if cloud provider cannot 662 * be fetched within the given timeout. 663 * @return {@link android.content.pm.ProviderInfo#authority authority} of the current 664 * {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider} 665 * integration is not enabled. This operation acquires a lock internally with a timeout. 666 */ 667 @Nullable getCloudProviderOrDefault(@ullable String defaultValue)668 public String getCloudProviderOrDefault(@Nullable String defaultValue) { 669 try { 670 return getCloudProviderWithTimeout(); 671 } catch (UnableToAcquireLockException e) { 672 Log.e(TAG, "Could not get cloud provider, returning default value: " + defaultValue, e); 673 return defaultValue; 674 } 675 } 676 677 /** 678 * @return {@link android.content.pm.ProviderInfo#authority authority} of the local provider. 679 */ 680 @NonNull getLocalProvider()681 public String getLocalProvider() { 682 return mLocalProvider; 683 } 684 685 /** 686 * Returns the local provider's collection info. 687 */ 688 @Nullable getLocalProviderLatestCollectionInfo()689 public ProviderCollectionInfo getLocalProviderLatestCollectionInfo() { 690 return getLatestCollectionInfoLocked(/* isLocal */ true, mLocalProvider); 691 } 692 693 /** 694 * Returns the current cloud provider's collection info. First, attempt to get it from cache. 695 * If the cache is not up to date, get it from the cloud provider directly. 696 */ 697 @Nullable getCloudProviderLatestCollectionInfo()698 public ProviderCollectionInfo getCloudProviderLatestCollectionInfo() { 699 try (CloseableReentrantLock ignored = mPickerSyncLockManager 700 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 701 final String currentCloudProvider = getCloudProviderWithTimeout(); 702 return getLatestCollectionInfoLocked(/* isLocal */ false, currentCloudProvider); 703 } catch (UnableToAcquireLockException e) { 704 Log.e(TAG, "Could not get latest collection info", e); 705 return null; 706 } 707 } 708 709 @Nullable getLatestCollectionInfoLocked( boolean isLocal, @Nullable String authority)710 private ProviderCollectionInfo getLatestCollectionInfoLocked( 711 boolean isLocal, 712 @Nullable String authority) { 713 final ProviderCollectionInfo latestCachedProviderCollectionInfo = 714 getLatestCollectionInfoLocked(isLocal); 715 716 if (latestCachedProviderCollectionInfo != null 717 && TextUtils.equals(authority, latestCachedProviderCollectionInfo.getAuthority())) { 718 Log.d(TAG, "Latest collection info up to date " + latestCachedProviderCollectionInfo); 719 return latestCachedProviderCollectionInfo; 720 } else { 721 final ProviderCollectionInfo latestCollectionInfo; 722 if (authority == null) { 723 return null; 724 } else { 725 Log.d(TAG, "Latest collection info up is NOT up to date. " 726 + "Fetching the latest collection info from CMP."); 727 final Bundle latestMediaCollectionInfoBundle = 728 getLatestMediaCollectionInfo(authority); 729 final String latestCollectionId = 730 latestMediaCollectionInfoBundle.getString(MEDIA_COLLECTION_ID); 731 final String latestAccountName = 732 latestMediaCollectionInfoBundle.getString(ACCOUNT_NAME); 733 final Intent latestAccountConfigurationIntent = 734 getAccountConfigurationIntent(latestMediaCollectionInfoBundle); 735 latestCollectionInfo = new ProviderCollectionInfo(authority, latestCollectionId, 736 latestAccountName, latestAccountConfigurationIntent); 737 } 738 739 updateLatestKnownCollectionInfoLocked(isLocal, latestCollectionInfo); 740 return (ProviderCollectionInfo) latestCollectionInfo.clone(); 741 } 742 } 743 744 @Nullable getAccountConfigurationIntent(@onNull Bundle bundle)745 private Intent getAccountConfigurationIntent(@NonNull Bundle bundle) { 746 if (SdkLevel.isAtLeastT()) { 747 return bundle.getParcelable(ACCOUNT_CONFIGURATION_INTENT, Intent.class); 748 } else { 749 return (Intent) bundle.getParcelable(ACCOUNT_CONFIGURATION_INTENT); 750 } 751 } 752 753 @Nullable getLatestCollectionInfoLocked(boolean isLocal)754 private ProviderCollectionInfo getLatestCollectionInfoLocked(boolean isLocal) { 755 ProviderCollectionInfo latestCollectionInfo; 756 if (isLocal) { 757 latestCollectionInfo = mLatestLocalProviderCollectionInfo; 758 } else { 759 latestCollectionInfo = mLatestCloudProviderCollectionInfo; 760 } 761 return latestCollectionInfo != null 762 ? (ProviderCollectionInfo) latestCollectionInfo.clone() 763 : null; 764 } 765 766 767 /** 768 * @return current cloud provider app localized label. This operation acquires a lock 769 * internally with a timeout. 770 * @throws UnableToAcquireLockException if the lock was not acquired within the given timeout. 771 */ getCurrentCloudProviderLocalizedLabel()772 public String getCurrentCloudProviderLocalizedLabel() throws UnableToAcquireLockException { 773 try (CloseableReentrantLock ignored = mPickerSyncLockManager 774 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 775 if (mCloudProviderInfo.isEmpty()) { 776 return mContext.getResources().getString(R.string.picker_settings_no_provider); 777 } 778 return CloudProviderUtils.getProviderLabel( 779 mContext.getPackageManager(), mCloudProviderInfo.authority); 780 } 781 } 782 isProviderEnabled(String authority)783 public boolean isProviderEnabled(String authority) { 784 if (mLocalProvider.equals(authority)) { 785 return true; 786 } 787 788 try (CloseableReentrantLock ignored = mPickerSyncLockManager 789 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 790 if (!mCloudProviderInfo.isEmpty() 791 && Objects.equals(mCloudProviderInfo.authority, authority)) { 792 return true; 793 } 794 } 795 796 return false; 797 } 798 isProviderEnabled(String authority, int uid)799 public boolean isProviderEnabled(String authority, int uid) { 800 if (uid == MY_UID && mLocalProvider.equals(authority)) { 801 return true; 802 } 803 804 try (CloseableReentrantLock ignored = mPickerSyncLockManager 805 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 806 if (!mCloudProviderInfo.isEmpty() && uid == mCloudProviderInfo.uid 807 && Objects.equals(mCloudProviderInfo.authority, authority)) { 808 return true; 809 } 810 } 811 812 return false; 813 } 814 isProviderSupported(String authority, int uid)815 public boolean isProviderSupported(String authority, int uid) { 816 if (uid == MY_UID && mLocalProvider.equals(authority)) { 817 return true; 818 } 819 820 // TODO(b/232738117): Enforce allow list here. This works around some CTS failure late in 821 // Android T. The current implementation is fine since cloud providers is only supported 822 // for app developers testing. 823 final List<CloudProviderInfo> infos = 824 CloudProviderUtils.getAllAvailableCloudProviders(mContext, mConfigStore); 825 for (CloudProviderInfo info : infos) { 826 if (info.uid == uid && Objects.equals(info.authority, authority)) { 827 return true; 828 } 829 } 830 831 return false; 832 } 833 834 /** 835 * Notifies about package removal 836 */ notifyPackageRemoval(String packageName)837 public void notifyPackageRemoval(String packageName) { 838 try (CloseableReentrantLock ignored = mPickerSyncLockManager 839 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 840 if (mCloudProviderInfo.matches(packageName)) { 841 Log.i(TAG, "Package " + packageName 842 + " is the current cloud provider and got removed"); 843 resetCloudProvider(); 844 } 845 } 846 } 847 848 @NonNull getSearchState()849 public SearchState getSearchState() { 850 return mSearchState; 851 } 852 resetCloudProvider()853 private void resetCloudProvider() { 854 try (CloseableReentrantLock ignored = mPickerSyncLockManager 855 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 856 setCloudProvider(/* authority */ null); 857 858 /** 859 * {@link #setCloudProvider(String null)} sets the cloud provider state to UNSET. 860 * Clearing the persisted cloud provider authority to set the state as NOT_SET instead. 861 */ 862 clearPersistedCloudProviderAuthority(); 863 864 initCloudProviderLocked(/* cachedAuthority */ null); 865 } 866 } 867 868 /** 869 * Syncs album media. 870 * 871 * @param enablePagedSync Set to true if the data from the provider may be synced in batches. 872 * If true, {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE 873 * is passed during query to the provider. 874 */ syncAlbumMediaFromProvider(String authority, boolean isLocal, String albumId, boolean enablePagedSync, @Nullable CancellationSignal cancellationSignal)875 private void syncAlbumMediaFromProvider(String authority, boolean isLocal, String albumId, 876 boolean enablePagedSync, @Nullable CancellationSignal cancellationSignal) { 877 final InstanceId instanceId = NonUiEventLogger.generateInstanceId(); 878 NonUiEventLogger.logPickerAlbumMediaSyncStart(instanceId, MY_UID, authority); 879 880 final Bundle queryArgs = new Bundle(); 881 queryArgs.putString(EXTRA_ALBUM_ID, albumId); 882 if (enablePagedSync) { 883 queryArgs.putInt(EXTRA_PAGE_SIZE, PAGE_SIZE); 884 } 885 886 Trace.beginSection(traceSectionName("syncAlbumMediaFromProvider", isLocal)); 887 try { 888 if (authority != null) { 889 executeSyncAddAlbum( 890 authority, isLocal, albumId, queryArgs, instanceId, cancellationSignal); 891 } 892 } catch (RuntimeException | UnableToAcquireLockException | WorkCancelledException e) { 893 // Unlike syncAllMediaFromProvider, we don't retry here because any errors would have 894 // occurred in fetching all the album_media since incremental sync is not supported. 895 // A full sync is therefore unlikely to resolve any issue 896 Log.e(TAG, "Failed to sync album media", e); 897 } catch (RequestObsoleteException e) { 898 Log.e(TAG, "Failed to sync all album media because authority has changed.", e); 899 executeSyncAlbumReset(authority, isLocal, albumId); 900 } finally { 901 Trace.endSection(); 902 } 903 } 904 905 /** 906 * Returns true if the sync was successful and the latest collection info was persisted. 907 * 908 * @param enablePagedSync Set to true if the data from the provider may be synced in batches. 909 * If true, {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} is passed 910 * during query to the provider. 911 */ syncAllMediaFromProvider( @ullable String authority, boolean isLocal, boolean retryOnFailure, boolean enablePagedSync, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal)912 private boolean syncAllMediaFromProvider( 913 @Nullable String authority, 914 boolean isLocal, 915 boolean retryOnFailure, 916 boolean enablePagedSync, 917 InstanceId instanceId, 918 @Nullable CancellationSignal cancellationSignal) { 919 Log.d(TAG, "syncAllMediaFromProvider() " + (isLocal ? "LOCAL" : "CLOUD") 920 + ", auth=" + authority 921 + ", retry=" + retryOnFailure); 922 if (DEBUG) { 923 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 924 } 925 926 Trace.beginSection(traceSectionName("syncAllMediaFromProvider", isLocal)); 927 try { 928 final SyncRequestParams params = getSyncRequestParams(authority, isLocal); 929 switch (params.syncType) { 930 case SYNC_TYPE_MEDIA_RESET: 931 // Can only happen when |authority| has been set to null and we need to clean up 932 disablePickerCloudMediaQueries(isLocal); 933 return resetAllMedia(authority, isLocal); 934 case SYNC_TYPE_MEDIA_FULL_WITH_RESET: 935 disablePickerCloudMediaQueries(isLocal); 936 if (!resetAllMedia(authority, isLocal)) { 937 return false; 938 } 939 940 // Cache collection id with default generation id to prevent DB reset if full 941 // sync resumes the next time sync is triggered. 942 cacheMediaCollectionInfo( 943 authority, isLocal, 944 getDefaultGenerationCollectionInfo(params.latestMediaCollectionInfo)); 945 // Fall through to run full sync 946 case SYNC_TYPE_MEDIA_FULL: 947 NonUiEventLogger.logPickerFullSyncStart(instanceId, MY_UID, authority); 948 949 enablePickerCloudMediaQueries(authority, isLocal); 950 951 // Send UI refresh notification for any active picker sessions, as the 952 // UI data might be stale if a full sync needs to be run. 953 sendPickerUiRefreshNotification(/* isInitPending */ false); 954 955 final Bundle fullSyncQueryArgs = new Bundle(); 956 if (enablePagedSync) { 957 fullSyncQueryArgs.putInt(EXTRA_PAGE_SIZE, params.mPageSize); 958 } 959 // Pass a mutable empty bundle intentionally because it might be populated with 960 // the next page token as part of a query to a cloud provider supporting 961 // pagination 962 executeSyncAdd(authority, isLocal, params.getMediaCollectionId(), 963 /* isIncrementalSync */ false, fullSyncQueryArgs, 964 instanceId, cancellationSignal); 965 966 // Commit sync position 967 return cacheMediaCollectionInfo( 968 authority, isLocal, params.latestMediaCollectionInfo); 969 case SYNC_TYPE_MEDIA_INCREMENTAL: 970 enablePickerCloudMediaQueries(authority, isLocal); 971 NonUiEventLogger.logPickerIncrementalSyncStart(instanceId, MY_UID, authority); 972 final Bundle queryArgs = new Bundle(); 973 queryArgs.putLong(EXTRA_SYNC_GENERATION, params.syncGeneration); 974 if (enablePagedSync) { 975 queryArgs.putInt(EXTRA_PAGE_SIZE, params.mPageSize); 976 } 977 978 executeSyncAdd( 979 authority, 980 isLocal, 981 params.getMediaCollectionId(), 982 /* isIncrementalSync */ true, 983 queryArgs, 984 instanceId, 985 cancellationSignal); 986 executeSyncRemove(authority, isLocal, params.getMediaCollectionId(), queryArgs, 987 instanceId, cancellationSignal); 988 989 // Commit sync position 990 return cacheMediaCollectionInfo( 991 authority, isLocal, params.latestMediaCollectionInfo); 992 case SYNC_TYPE_NONE: 993 enablePickerCloudMediaQueries(authority, isLocal); 994 return true; 995 default: 996 throw new IllegalArgumentException("Unexpected sync type: " + params.syncType); 997 } 998 } catch (WorkCancelledException e) { 999 // Do not reset picker DB here so that the sync operation resumes the next time sync is 1000 // triggered. 1001 Log.e(TAG, "Failed to sync all media because the sync was cancelled.", e); 1002 } catch (RequestObsoleteException e) { 1003 Log.e(TAG, "Failed to sync all media because authority has changed.", e); 1004 try { 1005 resetAllMedia(authority, isLocal); 1006 } catch (UnableToAcquireLockException ex) { 1007 Log.e(TAG, "Could not reset media", e); 1008 } 1009 } catch (IllegalStateException e) { 1010 // If we're in an illegal state, reset and start a full sync again. 1011 Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e); 1012 try { 1013 resetAllMedia(authority, isLocal); 1014 if (retryOnFailure) { 1015 return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false, 1016 enablePagedSync, instanceId, cancellationSignal); 1017 } 1018 } catch (UnableToAcquireLockException ex) { 1019 Log.e(TAG, "Could not reset media", e); 1020 } 1021 } catch (RuntimeException | UnableToAcquireLockException e) { 1022 // Retry the failed operation to see if it was an intermittent problem. If this fails, 1023 // the database will be in a partial state until the sync resumes from this point 1024 // on next run. 1025 Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e); 1026 if (retryOnFailure) { 1027 return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false, 1028 enablePagedSync, instanceId, cancellationSignal); 1029 } 1030 } finally { 1031 Trace.endSection(); 1032 } 1033 return false; 1034 } 1035 1036 /** 1037 * Disable cloud media queries from Picker database. After disabling cloud media queries, when a 1038 * media query will run on Picker database, only local media items will be returned. 1039 */ disablePickerCloudMediaQueries(boolean isLocal)1040 private void disablePickerCloudMediaQueries(boolean isLocal) 1041 throws UnableToAcquireLockException { 1042 if (!isLocal) { 1043 mDbFacade.setCloudProviderWithTimeout(null); 1044 } 1045 } 1046 1047 /** 1048 * Enable cloud media queries from Picker database. After enabling cloud media queries, when a 1049 * media query will run on Picker database, both local and cloud media items will be returned. 1050 */ enablePickerCloudMediaQueries(String authority, boolean isLocal)1051 private void enablePickerCloudMediaQueries(String authority, boolean isLocal) 1052 throws UnableToAcquireLockException { 1053 if (!isLocal) { 1054 try (CloseableReentrantLock ignored = mPickerSyncLockManager 1055 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 1056 if (Objects.equals(mCloudProviderInfo.authority, authority)) { 1057 mDbFacade.setCloudProviderWithTimeout(authority); 1058 } 1059 } 1060 } 1061 } 1062 executeSyncReset(String authority, boolean isLocal)1063 private void executeSyncReset(String authority, boolean isLocal) { 1064 Log.i(TAG, "Executing SyncReset. isLocal: " + isLocal + ". authority: " + authority); 1065 1066 Trace.beginSection(traceSectionName("executeSyncReset", isLocal)); 1067 try (PickerDbFacade.DbWriteOperation operation = 1068 mDbFacade.beginResetMediaOperation(authority)) { 1069 final int writeCount = operation.execute(null /* cursor */); 1070 operation.setSuccess(); 1071 1072 PickerNotificationSender.notifyMediaChange(mContext); 1073 1074 Log.i(TAG, "SyncReset. isLocal:" + isLocal + ". authority: " + authority 1075 + ". result count: " + writeCount); 1076 } finally { 1077 Trace.endSection(); 1078 } 1079 } 1080 executeSyncAlbumReset(String authority, boolean isLocal, String albumId)1081 private void executeSyncAlbumReset(String authority, boolean isLocal, String albumId) { 1082 Log.i(TAG, "Executing SyncAlbumReset." 1083 + " isLocal: " + isLocal + ". authority: " + authority + ". albumId: " + albumId); 1084 1085 Trace.beginSection(traceSectionName("executeSyncAlbumReset", isLocal)); 1086 try (PickerDbFacade.DbWriteOperation operation = 1087 mDbFacade.beginResetAlbumMediaOperation(authority, albumId)) { 1088 final int writeCount = operation.execute(null /* cursor */); 1089 operation.setSuccess(); 1090 1091 Log.i(TAG, "Successfully executed SyncResetAlbum. authority: " + authority 1092 + ". albumId: " + albumId + ". Result count: " + writeCount); 1093 } finally { 1094 Trace.endSection(); 1095 } 1096 } 1097 1098 /** 1099 * Queries the provider and adds media to the picker database. 1100 * 1101 * @param authority Provider's authority 1102 * @param isLocal Whether this is the local provider or not 1103 * @param expectedMediaCollectionId The MediaCollectionId from the last sync point. 1104 * @param isIncrementalSync If true, {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION} 1105 * should be honoured by the provider. 1106 * @param queryArgs Query arguments to pass in query. 1107 * @param instanceId Metrics related Picker session instance Id. 1108 * @param cancellationSignal CancellationSignal used to abort the sync. 1109 * @throws RequestObsoleteException When the sync is interrupted due to the provider 1110 * changing. 1111 */ executeSyncAdd( String authority, boolean isLocal, String expectedMediaCollectionId, boolean isIncrementalSync, Bundle queryArgs, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal)1112 private void executeSyncAdd( 1113 String authority, 1114 boolean isLocal, 1115 String expectedMediaCollectionId, 1116 boolean isIncrementalSync, 1117 Bundle queryArgs, 1118 InstanceId instanceId, 1119 @Nullable CancellationSignal cancellationSignal) 1120 throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { 1121 final Uri uri = getMediaUri(authority); 1122 final List<String> expectedHonoredArgs = new ArrayList<>(); 1123 if (isIncrementalSync) { 1124 expectedHonoredArgs.add(EXTRA_SYNC_GENERATION); 1125 } 1126 1127 Log.i(TAG, "Executing SyncAdd. isLocal: " + isLocal + ". authority: " + authority); 1128 1129 String resumeKey = 1130 getPrefsKey(isLocal, PREFS_KEY_OPERATION_MEDIA_ADD_PREFIX + PREFS_KEY_RESUME); 1131 1132 Trace.beginSection(traceSectionName("executeSyncAdd", isLocal)); 1133 try { 1134 int syncedItems = executePagedSync( 1135 uri, 1136 expectedMediaCollectionId, 1137 expectedHonoredArgs, 1138 queryArgs, 1139 resumeKey, 1140 OPERATION_ADD_MEDIA, 1141 authority, 1142 isLocal, 1143 cancellationSignal); 1144 NonUiEventLogger.logPickerAddMediaSyncCompletion(instanceId, MY_UID, authority, 1145 syncedItems); 1146 } finally { 1147 Trace.endSection(); 1148 } 1149 } 1150 1151 /** 1152 * Queries the provider to sync media from the given albumId into the picker database. 1153 * 1154 * @param authority Provider's authority 1155 * @param isLocal Whether this is the local provider or not 1156 * @param albumId the Id of the album to sync 1157 * @param queryArgs Query arguments to pass in query. 1158 * @param instanceId Metrics related Picker session instance Id. 1159 * @param cancellationSignal CancellationSignal used to abort the sync. 1160 * @throws RequestObsoleteException When the sync is interrupted due to the provider 1161 * changing. 1162 */ executeSyncAddAlbum( String authority, boolean isLocal, String albumId, Bundle queryArgs, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal)1163 private void executeSyncAddAlbum( 1164 String authority, 1165 boolean isLocal, 1166 String albumId, 1167 Bundle queryArgs, 1168 InstanceId instanceId, 1169 @Nullable CancellationSignal cancellationSignal) 1170 throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { 1171 final Uri uri = getMediaUri(authority); 1172 1173 Log.i(TAG, "Executing SyncAddAlbum. " 1174 + "isLocal: " + isLocal + ". authority: " + authority + ". albumId: " + albumId); 1175 String resumeKey = 1176 getPrefsKey(isLocal, PREFS_KEY_OPERATION_ALBUM_ADD_PREFIX + PREFS_KEY_RESUME); 1177 1178 Trace.beginSection(traceSectionName("executeSyncAddAlbum", isLocal)); 1179 try { 1180 1181 // We don't need to validate the mediaCollectionId for album_media sync since it's 1182 // always a full sync 1183 int syncedItems = 1184 executePagedSync( 1185 uri, /* mediaCollectionId */ 1186 null, 1187 List.of(EXTRA_ALBUM_ID), 1188 queryArgs, 1189 resumeKey, 1190 OPERATION_ADD_ALBUM, 1191 authority, 1192 isLocal, 1193 albumId, 1194 /*cancellationSignal=*/ cancellationSignal); 1195 NonUiEventLogger.logPickerAddAlbumMediaSyncCompletion(instanceId, MY_UID, authority, 1196 syncedItems); 1197 } finally { 1198 Trace.endSection(); 1199 } 1200 } 1201 1202 /** 1203 * Queries the provider and syncs removed media with the picker database. 1204 * 1205 * @param authority Provider's authority 1206 * @param isLocal Whether this is the local provider or not 1207 * @param mediaCollectionId The last synced media collection id 1208 * @param queryArgs Query arguments to pass in query. 1209 * @param instanceId Metrics related Picker session instance Id. 1210 * @param cancellationSignal CancellationSignal used to abort the sync. 1211 * @throws RequestObsoleteException When the sync is interrupted due to the provider 1212 * changing. 1213 */ executeSyncRemove( String authority, boolean isLocal, String mediaCollectionId, Bundle queryArgs, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal)1214 private void executeSyncRemove( 1215 String authority, 1216 boolean isLocal, 1217 String mediaCollectionId, 1218 Bundle queryArgs, 1219 InstanceId instanceId, 1220 @Nullable CancellationSignal cancellationSignal) 1221 throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { 1222 final Uri uri = getDeletedMediaUri(authority); 1223 1224 Log.i(TAG, "Executing SyncRemove. isLocal: " + isLocal + ". authority: " + authority); 1225 String resumeKey = 1226 getPrefsKey(isLocal, PREFS_KEY_OPERATION_MEDIA_REMOVE_PREFIX + PREFS_KEY_RESUME); 1227 1228 Trace.beginSection(traceSectionName("executeSyncRemove", isLocal)); 1229 try { 1230 int syncedItems = 1231 executePagedSync( 1232 uri, 1233 mediaCollectionId, 1234 List.of(EXTRA_SYNC_GENERATION), 1235 queryArgs, 1236 resumeKey, 1237 OPERATION_REMOVE_MEDIA, 1238 authority, 1239 isLocal, 1240 cancellationSignal); 1241 NonUiEventLogger.logPickerRemoveMediaSyncCompletion(instanceId, MY_UID, authority, 1242 syncedItems); 1243 } finally { 1244 Trace.endSection(); 1245 } 1246 } 1247 1248 /** 1249 * Persist cloud provider info and send a sync request to the background thread. 1250 */ persistCloudProviderInfo(@onNull CloudProviderInfo info, boolean shouldUnset)1251 private void persistCloudProviderInfo(@NonNull CloudProviderInfo info, boolean shouldUnset) { 1252 try (CloseableReentrantLock ignored = mPickerSyncLockManager 1253 .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 1254 setCurrentCloudProviderInfo(info); 1255 1256 final String authority = info.authority; 1257 final SharedPreferences.Editor editor = mUserPrefs.edit(); 1258 final boolean isCloudProviderInfoNotEmpty = !info.isEmpty(); 1259 1260 if (isCloudProviderInfoNotEmpty) { 1261 editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, authority); 1262 } else if (shouldUnset) { 1263 editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, 1264 PREFS_VALUE_CLOUD_PROVIDER_UNSET); 1265 } else { 1266 editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY); 1267 } 1268 1269 editor.apply(); 1270 1271 if (SdkLevel.isAtLeastT()) { 1272 try { 1273 StorageManager sm = mContext.getSystemService(StorageManager.class); 1274 sm.setCloudMediaProvider(authority); 1275 } catch (SecurityException e) { 1276 // When run as part of the unit tests, the notification fails because only the 1277 // MediaProvider uid can notify 1278 Log.w(TAG, "Failed to notify the system of cloud provider update to: " 1279 + authority); 1280 } 1281 } 1282 1283 Log.d(TAG, "Updated cloud provider to: " + authority); 1284 1285 try { 1286 resetCachedMediaCollectionInfo(info.authority, /* isLocal */ false); 1287 } catch (UnableToAcquireLockException e) { 1288 Log.wtf(TAG, "CLOUD_PROVIDER_LOCK is already held by this thread."); 1289 } 1290 1291 sendPickerUiRefreshNotification(/* isInitPending */ true); 1292 1293 // We need this to trigger a sync from the UI 1294 PickerNotificationSender.notifyAvailableProvidersChange(mContext); 1295 updateLatestKnownCollectionInfoLocked(false, null); 1296 } 1297 } 1298 1299 /** 1300 * Send Picker UI content observers a notification that a refresh is required. 1301 * @param isInitPending when true, appends the URI path segment 1302 * {@link com.android.providers.media.PickerUriResolver.INIT_PATH} to the notification URI 1303 * to indicate that the UI that the cached picker data might be stale. 1304 * When a request notification is being sent from the sync path, set isInitPending as false to 1305 * prevent sending refresh notification in a loop. 1306 */ sendPickerUiRefreshNotification(boolean isInitPending)1307 private void sendPickerUiRefreshNotification(boolean isInitPending) { 1308 final ContentResolver contentResolver = mContext.getContentResolver(); 1309 if (contentResolver != null) { 1310 final Uri.Builder builder = REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI.buildUpon(); 1311 if (isInitPending) { 1312 builder.appendPath(INIT_PATH); 1313 } 1314 final Uri refreshUri = builder.build(); 1315 contentResolver.notifyChange(refreshUri, null); 1316 } else { 1317 Log.d(TAG, "Couldn't notify the Picker UI to refresh"); 1318 } 1319 } 1320 1321 /** 1322 * Clears the persisted cloud provider authority and sets the state to default (NOT_SET). 1323 */ 1324 @VisibleForTesting clearPersistedCloudProviderAuthority()1325 void clearPersistedCloudProviderAuthority() { 1326 Log.d(TAG, "Setting the cloud provider state to default (NOT_SET) by clearing the " 1327 + "persisted cloud provider authority"); 1328 mUserPrefs.edit().remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY).apply(); 1329 } 1330 1331 /** 1332 * Commit the latest media collection info when a sync operation is completed. 1333 */ cacheMediaCollectionInfo(@ullable String authority, boolean isLocal, @Nullable Bundle bundle)1334 public boolean cacheMediaCollectionInfo(@Nullable String authority, boolean isLocal, 1335 @Nullable Bundle bundle) throws UnableToAcquireLockException { 1336 if (authority == null) { 1337 Log.d(TAG, "Ignoring cache media info for null authority with bundle: " + bundle); 1338 return true; 1339 } 1340 1341 Trace.beginSection(traceSectionName("cacheMediaCollectionInfo", isLocal)); 1342 1343 try { 1344 if (isLocal) { 1345 cacheMediaCollectionInfoInternal(isLocal, bundle); 1346 return true; 1347 } else { 1348 try (CloseableReentrantLock ignored = mPickerSyncLockManager 1349 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 1350 // Check if the media collection info belongs to the current cloud provider 1351 // authority. 1352 if (Objects.equals(authority, mCloudProviderInfo.authority)) { 1353 cacheMediaCollectionInfoInternal(isLocal, bundle); 1354 return true; 1355 } else { 1356 Log.e(TAG, "Do not cache collection info for " 1357 + authority + " because cloud provider changed to " 1358 + mCloudProviderInfo.authority); 1359 return false; 1360 } 1361 } 1362 } 1363 } finally { 1364 Trace.endSection(); 1365 } 1366 } 1367 cacheMediaCollectionInfoInternal(boolean isLocal, @Nullable Bundle bundle)1368 private void cacheMediaCollectionInfoInternal(boolean isLocal, 1369 @Nullable Bundle bundle) { 1370 final SharedPreferences.Editor editor = mSyncPrefs.edit(); 1371 if (bundle == null) { 1372 editor.remove(getPrefsKey(isLocal, MEDIA_COLLECTION_ID)); 1373 editor.remove(getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION)); 1374 // Clear any resume keys for page tokens. 1375 editor.remove( 1376 getPrefsKey(isLocal, PREFS_KEY_OPERATION_MEDIA_ADD_PREFIX + PREFS_KEY_RESUME)); 1377 editor.remove( 1378 getPrefsKey(isLocal, PREFS_KEY_OPERATION_ALBUM_ADD_PREFIX + PREFS_KEY_RESUME)); 1379 editor.remove( 1380 getPrefsKey( 1381 isLocal, PREFS_KEY_OPERATION_MEDIA_REMOVE_PREFIX + PREFS_KEY_RESUME)); 1382 } else { 1383 final String collectionId = bundle.getString(MEDIA_COLLECTION_ID); 1384 final long generation = bundle.getLong(LAST_MEDIA_SYNC_GENERATION); 1385 1386 editor.putString(getPrefsKey(isLocal, MEDIA_COLLECTION_ID), collectionId); 1387 editor.putLong(getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), generation); 1388 } 1389 editor.apply(); 1390 } 1391 1392 /** 1393 * Adds the given token to the saved sync preferences. 1394 * 1395 * @param token The token to remember. A null value will clear the preference. 1396 * @param resumeKey The operation's key in sync preferences. 1397 */ rememberNextPageToken(@ullable String token, String resumeKey)1398 private void rememberNextPageToken(@Nullable String token, String resumeKey) 1399 throws UnableToAcquireLockException { 1400 1401 try (CloseableReentrantLock ignored = mPickerSyncLockManager 1402 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 1403 final SharedPreferences.Editor editor = mSyncPrefs.edit(); 1404 if (token == null) { 1405 Log.d(TAG, String.format("Clearing next page token for key: %s", resumeKey)); 1406 editor.remove(resumeKey); 1407 } else { 1408 Log.d( 1409 TAG, 1410 String.format("Saving next page token: %s for key: %s", token, resumeKey)); 1411 editor.putString(resumeKey, token); 1412 } 1413 editor.apply(); 1414 } 1415 } 1416 1417 /** 1418 * Fetches the next page token given a resume key. Returns null if no NextPage token was saved. 1419 * 1420 * @param resumeKey The operation's resume key. 1421 * @return The PageToken to resume from, or {@code null} if there is no operation to resume. 1422 */ 1423 @Nullable getPageTokenFromResumeKey(String resumeKey)1424 private String getPageTokenFromResumeKey(String resumeKey) throws UnableToAcquireLockException { 1425 try (CloseableReentrantLock ignored = mPickerSyncLockManager 1426 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 1427 return mSyncPrefs.getString(resumeKey, /* defValue= */ null); 1428 } 1429 } 1430 resetCachedMediaCollectionInfo(@ullable String authority, boolean isLocal)1431 private boolean resetCachedMediaCollectionInfo(@Nullable String authority, boolean isLocal) 1432 throws UnableToAcquireLockException { 1433 return cacheMediaCollectionInfo(authority, isLocal, /* bundle */ null); 1434 } 1435 getCachedMediaCollectionInfo(boolean isLocal)1436 private Bundle getCachedMediaCollectionInfo(boolean isLocal) { 1437 final Bundle bundle = new Bundle(); 1438 1439 final String collectionId = mSyncPrefs.getString( 1440 getPrefsKey(isLocal, MEDIA_COLLECTION_ID), /* default */ null); 1441 final long generation = mSyncPrefs.getLong( 1442 getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), DEFAULT_GENERATION); 1443 1444 bundle.putString(MEDIA_COLLECTION_ID, collectionId); 1445 bundle.putLong(LAST_MEDIA_SYNC_GENERATION, generation); 1446 1447 return bundle; 1448 } 1449 1450 @NonNull getLatestMediaCollectionInfo(String authority)1451 private Bundle getLatestMediaCollectionInfo(String authority) { 1452 final InstanceId instanceId = NonUiEventLogger.generateInstanceId(); 1453 NonUiEventLogger.logPickerGetMediaCollectionInfoStart(instanceId, MY_UID, authority); 1454 try { 1455 Bundle result = mContext.getContentResolver().call(getMediaCollectionInfoUri(authority), 1456 CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, 1457 /* extras */ new Bundle()); 1458 return (result == null) ? (new Bundle()) : result; 1459 } finally { 1460 NonUiEventLogger.logPickerGetMediaCollectionInfoEnd(instanceId, MY_UID, authority); 1461 } 1462 } 1463 getDefaultGenerationCollectionInfo(@onNull Bundle latestCollectionInfo)1464 private Bundle getDefaultGenerationCollectionInfo(@NonNull Bundle latestCollectionInfo) { 1465 final Bundle bundle = new Bundle(); 1466 final String collectionId = latestCollectionInfo.getString(MEDIA_COLLECTION_ID); 1467 bundle.putString(MEDIA_COLLECTION_ID, collectionId); 1468 bundle.putLong(LAST_MEDIA_SYNC_GENERATION, DEFAULT_GENERATION); 1469 return bundle; 1470 } 1471 1472 /** 1473 * Checks if full sync is pending for the given CMP. 1474 * 1475 * @param authority Authority of the CMP that uniquely identifies it. 1476 * @param isLocal true of the authority belongs to the local provider, else false. 1477 * @return true if full sync is pending for the CMP, else false. 1478 * @throws RequestObsoleteException if the input authority is different than the authority of 1479 * the current cloud provider. 1480 */ isFullSyncPending(@onNull String authority, boolean isLocal)1481 public boolean isFullSyncPending(@NonNull String authority, boolean isLocal) 1482 throws RequestObsoleteException { 1483 final ProviderCollectionInfo latestCollectionInfo = isLocal 1484 ? getLocalProviderLatestCollectionInfo() 1485 : getCloudProviderLatestCollectionInfo(); 1486 1487 if (!authority.equals(latestCollectionInfo.getAuthority())) { 1488 throw new RequestObsoleteException( 1489 "Authority has changed to " + latestCollectionInfo.getAuthority()); 1490 } 1491 1492 final Bundle cachedPreviousCollectionInfo = getCachedMediaCollectionInfo(isLocal); 1493 final String cachedPreviousCollectionId = 1494 cachedPreviousCollectionInfo.getString(MEDIA_COLLECTION_ID); 1495 final long cachedPreviousGeneration = 1496 cachedPreviousCollectionInfo.getLong(LAST_MEDIA_SYNC_GENERATION); 1497 1498 return isFullSyncRequired( 1499 latestCollectionInfo.getCollectionId(), 1500 cachedPreviousCollectionId, 1501 cachedPreviousGeneration); 1502 } 1503 1504 @NonNull getSyncRequestParams(@ullable String authority, boolean isLocal)1505 private SyncRequestParams getSyncRequestParams(@Nullable String authority, 1506 boolean isLocal) throws RequestObsoleteException, UnableToAcquireLockException { 1507 if (isLocal) { 1508 return getSyncRequestParamsLocked(authority, isLocal); 1509 } else { 1510 // Ensure that we are fetching sync request params for the current cloud provider. 1511 try (CloseableReentrantLock ignored = mPickerSyncLockManager 1512 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 1513 if (Objects.equals(mCloudProviderInfo.authority, authority)) { 1514 return getSyncRequestParamsLocked(authority, isLocal); 1515 } else { 1516 throw new RequestObsoleteException("Attempt to fetch sync request params for an" 1517 + " unknown cloud provider. Current provider: " 1518 + mCloudProviderInfo.authority + " Requested provider: " + authority); 1519 } 1520 } 1521 } 1522 } 1523 1524 @NonNull getSyncRequestParamsLocked(@ullable String authority, boolean isLocal)1525 private SyncRequestParams getSyncRequestParamsLocked(@Nullable String authority, 1526 boolean isLocal) { 1527 Log.d(TAG, "getSyncRequestParams() " + (isLocal ? "LOCAL" : "CLOUD") 1528 + ", auth=" + authority); 1529 if (DEBUG) { 1530 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 1531 } 1532 1533 final SyncRequestParams result; 1534 if (authority == null) { 1535 // Only cloud authority can be null 1536 result = SyncRequestParams.forResetMedia(); 1537 } else { 1538 final Bundle cachedMediaCollectionInfo = getCachedMediaCollectionInfo(isLocal); 1539 final Bundle latestMediaCollectionInfo = getLatestMediaCollectionInfo(authority); 1540 1541 final String latestCollectionId = 1542 latestMediaCollectionInfo.getString(MEDIA_COLLECTION_ID); 1543 final long latestGeneration = 1544 latestMediaCollectionInfo.getLong(LAST_MEDIA_SYNC_GENERATION); 1545 Log.d(TAG, " Latest ID/Gen=" + latestCollectionId + "/" + latestGeneration); 1546 1547 final String cachedCollectionId = 1548 cachedMediaCollectionInfo.getString(MEDIA_COLLECTION_ID); 1549 final long cachedGeneration = 1550 cachedMediaCollectionInfo.getLong(LAST_MEDIA_SYNC_GENERATION); 1551 Log.d(TAG, " Cached ID/Gen=" + cachedCollectionId + "/" + cachedGeneration); 1552 1553 if (TextUtils.isEmpty(latestCollectionId) || latestGeneration < 0) { 1554 throw new IllegalStateException("Unexpected Latest Media Collection Info: " 1555 + "ID/Gen=" + latestCollectionId + "/" + latestGeneration); 1556 } 1557 1558 if (isFullSyncWithResetRequired(latestCollectionId, cachedCollectionId)) { 1559 result = SyncRequestParams.forFullMediaWithReset(latestMediaCollectionInfo); 1560 1561 // Update collection info cache. 1562 final String latestAccountName = 1563 latestMediaCollectionInfo.getString(ACCOUNT_NAME); 1564 final Intent latestAccountConfigurationIntent = 1565 getAccountConfigurationIntent(latestMediaCollectionInfo); 1566 final ProviderCollectionInfo latestCollectionInfo = 1567 new ProviderCollectionInfo(authority, latestCollectionId, latestAccountName, 1568 latestAccountConfigurationIntent); 1569 updateLatestKnownCollectionInfoLocked(isLocal, latestCollectionInfo); 1570 } else if (isFullSyncWithoutResetRequired( 1571 latestCollectionId, cachedCollectionId, cachedGeneration)) { 1572 result = SyncRequestParams.forFullMedia(latestMediaCollectionInfo); 1573 } else if (cachedGeneration == latestGeneration) { 1574 result = SyncRequestParams.forNone(); 1575 } else { 1576 result = SyncRequestParams.forIncremental( 1577 cachedGeneration, latestMediaCollectionInfo); 1578 } 1579 } 1580 Log.d(TAG, " RESULT=" + result); 1581 return result; 1582 } 1583 1584 /** 1585 * @param latestCollectionId The latest collection id of the CMP library. 1586 * @param cachedCollectionId The last collection id Picker DB was synced with, either fully 1587 * or partially. 1588 * @param cachedGenerationId The last generation id Picker DB was synced with. 1589 * @return true if a full sync is pending, else false. 1590 */ isFullSyncRequired( @ullable String latestCollectionId, @Nullable String cachedCollectionId, long cachedGenerationId)1591 private boolean isFullSyncRequired( 1592 @Nullable String latestCollectionId, 1593 @Nullable String cachedCollectionId, 1594 long cachedGenerationId) { 1595 return isFullSyncWithResetRequired(latestCollectionId, cachedCollectionId) 1596 || isFullSyncWithoutResetRequired(latestCollectionId, cachedCollectionId, 1597 cachedGenerationId); 1598 } 1599 1600 /** 1601 * @param latestCollectionId The latest collection id of the CMP library. 1602 * @param cachedCollectionId The last collection id Picker DB was synced with, either fully 1603 * or partially. 1604 * @return true if a full sync with reset is pending, else false. 1605 */ isFullSyncWithResetRequired( @ullable String latestCollectionId, @Nullable String cachedCollectionId)1606 private boolean isFullSyncWithResetRequired( 1607 @Nullable String latestCollectionId, 1608 @Nullable String cachedCollectionId) { 1609 return !Objects.equals(latestCollectionId, cachedCollectionId); 1610 } 1611 1612 /** 1613 * @param latestCollectionId The latest collection id of the CMP library. 1614 * @param cachedCollectionId The last collection id Picker DB was synced with, either fully 1615 * or partially. 1616 * @param cachedGenerationId The last generation id Picker DB was synced with. 1617 * @return true if a resumable full sync is pending, else false. 1618 */ isFullSyncWithoutResetRequired( @ullable String latestCollectionId, @Nullable String cachedCollectionId, long cachedGenerationId)1619 private boolean isFullSyncWithoutResetRequired( 1620 @Nullable String latestCollectionId, 1621 @Nullable String cachedCollectionId, 1622 long cachedGenerationId) { 1623 return Objects.equals(latestCollectionId, cachedCollectionId) 1624 && cachedGenerationId == DEFAULT_GENERATION; 1625 } 1626 updateLatestKnownCollectionInfoLocked( boolean isLocal, @Nullable ProviderCollectionInfo latestCollectionInfo)1627 private void updateLatestKnownCollectionInfoLocked( 1628 boolean isLocal, 1629 @Nullable ProviderCollectionInfo latestCollectionInfo) { 1630 if (isLocal) { 1631 mLatestLocalProviderCollectionInfo = latestCollectionInfo; 1632 } else { 1633 mLatestCloudProviderCollectionInfo = latestCollectionInfo; 1634 } 1635 } 1636 1637 /** 1638 * Generates a key for shared preferences by appending the specified key 1639 * to the prefix corresponding to the local or cloud provider. 1640 * 1641 * @param isLocal {@code true} to use the local provider prefix, 1642 * {@code false} to use the cloud provider prefix. 1643 * @param key the specific key to append to the provider's prefix. 1644 * @return a complete key to used to query shared preferences. 1645 */ getPrefsKey(boolean isLocal, String key)1646 public static String getPrefsKey(boolean isLocal, String key) { 1647 return (isLocal ? PREFS_KEY_LOCAL_PREFIX : PREFS_KEY_CLOUD_PREFIX) + key; 1648 } 1649 query(Uri uri, Bundle extras)1650 private Cursor query(Uri uri, Bundle extras) { 1651 return mContext.getContentResolver().query(uri, /* projection */ null, extras, 1652 /* cancellationSignal */ null); 1653 } 1654 1655 /** 1656 * Creates a matching {@link PickerDbFacade.DbWriteOperation} for the given 1657 * {@link OperationType}. 1658 * 1659 * @param op {@link OperationType} Which type of paged operation to begin. 1660 * @param authority The authority string of the sync provider. 1661 * @param albumId An {@link Nullable} AlbumId for album related operations. 1662 * @throws IllegalArgumentException When an unexpected op type is encountered. 1663 */ beginPagedOperation( @perationType int op, String authority, @Nullable String albumId)1664 private PickerDbFacade.DbWriteOperation beginPagedOperation( 1665 @OperationType int op, String authority, @Nullable String albumId) 1666 throws IllegalArgumentException { 1667 switch (op) { 1668 case OPERATION_ADD_MEDIA: 1669 return mDbFacade.beginAddMediaOperation(authority); 1670 case OPERATION_ADD_ALBUM: 1671 Objects.requireNonNull( 1672 albumId, "Cannot begin an AddAlbum operation without albumId"); 1673 return mDbFacade.beginAddAlbumMediaOperation(authority, albumId); 1674 case OPERATION_REMOVE_MEDIA: 1675 return mDbFacade.beginRemoveMediaOperation(authority); 1676 default: 1677 throw new IllegalArgumentException( 1678 "Cannot begin a paged operation without an expected operation type."); 1679 } 1680 } 1681 1682 /** 1683 * Executes a page-by-page sync from the provider. 1684 * 1685 * @param uri The uri to query for a cursor. 1686 * @param expectedMediaCollectionId The expected media collection id. 1687 * @param expectedHonoredArgs The arguments that are expected to be present in cursors fetched 1688 * from the provider. 1689 * @param queryArgs Any query arguments that are to be passed to the provider when fetching the 1690 * cursor. 1691 * @param resumeKey The resumable operation key. This is used to check for previously failed 1692 * operations so they can be resumed at the last successful page, and also to save progress 1693 * between pages. 1694 * @param op The DbWriteOperation type. {@link OperationType} 1695 * @param authority The authority string of the provider to sync with. 1696 * @param cancellationSignal CancellationSignal used to abort the sync. 1697 * @throws RequestObsoleteException When the sync is interrupted due to the provider 1698 * changing. 1699 * @return the total number of rows synced. 1700 */ executePagedSync( Uri uri, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Bundle queryArgs, @Nullable String resumeKey, @OperationType int op, String authority, Boolean isLocal, @Nullable CancellationSignal cancellationSignal)1701 private int executePagedSync( 1702 Uri uri, 1703 String expectedMediaCollectionId, 1704 List<String> expectedHonoredArgs, 1705 Bundle queryArgs, 1706 @Nullable String resumeKey, 1707 @OperationType int op, 1708 String authority, 1709 Boolean isLocal, 1710 @Nullable CancellationSignal cancellationSignal) 1711 throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { 1712 return executePagedSync( 1713 uri, 1714 expectedMediaCollectionId, 1715 expectedHonoredArgs, 1716 queryArgs, 1717 resumeKey, 1718 op, 1719 authority, 1720 isLocal, 1721 /* albumId=*/ null, 1722 cancellationSignal); 1723 } 1724 1725 /** 1726 * Executes a page-by-page sync from the provider. 1727 * 1728 * @param uri The uri to query for a cursor. 1729 * @param expectedMediaCollectionId The expected media collection id. 1730 * @param expectedHonoredArgs The arguments that are expected to be present in cursors fetched 1731 * from the provider. 1732 * @param queryArgs Any query arguments that are to be passed to the provider when fetching the 1733 * cursor. 1734 * @param resumeKey The resumable operation key. This is used to check for previously failed 1735 * operations so they can be resumed at the last successful page, and also to save progress 1736 * between pages. 1737 * @param op The DbWriteOperation type. {@link OperationType} 1738 * @param authority The authority string of the provider to sync with. 1739 * @param albumId A {@link Nullable} albumId for album related operations. 1740 * @param cancellationSignal CancellationSignal used to abort the sync. 1741 * @throws RequestObsoleteException When the sync is interrupted due to the provider 1742 * changing. 1743 * @return the total number of rows synced. 1744 */ executePagedSync( Uri uri, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Bundle queryArgs, @Nullable String resumeKey, @OperationType int op, String authority, Boolean isLocal, @Nullable String albumId, @Nullable CancellationSignal cancellationSignal)1745 private int executePagedSync( 1746 Uri uri, 1747 String expectedMediaCollectionId, 1748 List<String> expectedHonoredArgs, 1749 Bundle queryArgs, 1750 @Nullable String resumeKey, 1751 @OperationType int op, 1752 String authority, 1753 Boolean isLocal, 1754 @Nullable String albumId, 1755 @Nullable CancellationSignal cancellationSignal) 1756 throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { 1757 Trace.beginSection(traceSectionName("executePagedSync")); 1758 1759 try { 1760 int totalRowcount = 0; 1761 // Set to check the uniqueness of tokens across pages. 1762 Set<String> tokens = new ArraySet<>(); 1763 1764 String nextPageToken = getPageTokenFromResumeKey(resumeKey); 1765 if (nextPageToken != null) { 1766 Log.i( 1767 TAG, 1768 String.format( 1769 "Resumable operation found for %s, resuming with page token %s", 1770 resumeKey, nextPageToken)); 1771 } 1772 1773 do { 1774 // At the top of each loop check to see if we've received a CancellationSignal 1775 // to stop the paged sync. 1776 if (cancellationSignal != null && cancellationSignal.isCanceled()) { 1777 throw new WorkCancelledException( 1778 "Aborting sync: cancellationSignal was received"); 1779 } 1780 1781 String updateDateTakenMs = null; 1782 if (nextPageToken != null) { 1783 queryArgs.putString(EXTRA_PAGE_TOKEN, nextPageToken); 1784 } 1785 1786 try (Cursor cursor = query(uri, queryArgs)) { 1787 nextPageToken = 1788 validateCursor( 1789 cursor, expectedMediaCollectionId, expectedHonoredArgs, tokens); 1790 1791 try (PickerDbFacade.DbWriteOperation operation = 1792 beginPagedOperation(op, authority, albumId)) { 1793 int writeCount = operation.execute(cursor); 1794 1795 if (!isLocal) { 1796 // Ensure the cloud provider hasn't change out from underneath the 1797 // running sync. If it has, we need to stop syncing. 1798 String currentCloudProvider = getCloudProviderWithTimeout(); 1799 if (TextUtils.isEmpty(currentCloudProvider) 1800 || !currentCloudProvider.equals(authority)) { 1801 1802 throw new RequestObsoleteException( 1803 String.format( 1804 "Aborting sync: the CloudProvider seems to have" 1805 + " changed mid-sync. Old: %s Current: %s", 1806 authority, currentCloudProvider)); 1807 } 1808 } 1809 1810 operation.setSuccess(); 1811 totalRowcount += writeCount; 1812 1813 if (cursor.getCount() > 0) { 1814 // Before the cursor is closed pull the date taken ms for the first row. 1815 updateDateTakenMs = getFirstDateTakenMsInCursor(cursor); 1816 1817 // If the cursor count is not null and the date taken field is not 1818 // present in the cursor, fallback on the operation to provide the date 1819 // taken. 1820 if (updateDateTakenMs == null) { 1821 updateDateTakenMs = getFirstDateTakenMsFromOperation(operation); 1822 } 1823 } 1824 } 1825 } catch (IllegalArgumentException ex) { 1826 Log.e(TAG, String.format("Failed to open DbWriteOperation for op: %d", op), ex); 1827 return -1; 1828 } 1829 1830 // Keep track of the next page token in case this operation crashes and is 1831 // later resumed. 1832 rememberNextPageToken(nextPageToken, resumeKey); 1833 1834 // Emit notification that new data has arrived in the database. 1835 if (updateDateTakenMs != null) { 1836 Uri notification = buildNotificationUri(op, albumId, updateDateTakenMs); 1837 1838 if (notification != null) { 1839 mContext.getContentResolver() 1840 .notifyChange(/* itemUri= */ notification, /* observer= */ null); 1841 } 1842 } 1843 1844 // Only send a media update notification if the media table is getting updated. 1845 if (albumId == null) { 1846 PickerNotificationSender.notifyMediaChange(mContext); 1847 } else { 1848 PickerNotificationSender.notifyAlbumMediaChange(mContext, authority, albumId); 1849 } 1850 } while (nextPageToken != null); 1851 1852 Log.i( 1853 TAG, 1854 "Paged sync successful. QueryArgs: " 1855 + queryArgs 1856 + " Total Rows: " 1857 + totalRowcount); 1858 return totalRowcount; 1859 } finally { 1860 Trace.endSection(); 1861 } 1862 } 1863 1864 /** 1865 * Extracts the {@link MediaColumns.DATE_TAKEN_MILLIS} from the first row in the cursor. 1866 * 1867 * @param cursor The cursor to read from. 1868 * @return Either the column value if it exists, or {@code null} if it doesn't. 1869 */ 1870 @Nullable getFirstDateTakenMsInCursor(Cursor cursor)1871 private String getFirstDateTakenMsInCursor(Cursor cursor) { 1872 if (cursor.moveToFirst()) { 1873 return getCursorString(cursor, MediaColumns.DATE_TAKEN_MILLIS); 1874 } 1875 return null; 1876 } 1877 1878 /** 1879 * Extracts the first row's date taken from the operation. Note that all functions may not 1880 * implement this method. 1881 */ getFirstDateTakenMsFromOperation(PickerDbFacade.DbWriteOperation op)1882 private String getFirstDateTakenMsFromOperation(PickerDbFacade.DbWriteOperation op) { 1883 final long firstDateTakenMillis = op.getFirstDateTakenMillis(); 1884 1885 return firstDateTakenMillis == Long.MIN_VALUE 1886 ? null 1887 : Long.toString(firstDateTakenMillis); 1888 } 1889 1890 /** 1891 * Assembles a ContentObserver notification uri for the given operation. 1892 * 1893 * @param op {@link OperationType} the operation to notify has completed. 1894 * @param albumId An optional album id if this is an album based operation. 1895 * @param dateTakenMs The notification data; the {@link MediaColumns.DATE_TAKEN_MILLIS} of the 1896 * first row updated. 1897 * @return the assembled notification uri. 1898 */ 1899 @Nullable buildNotificationUri( @onNull @perationType int op, @Nullable String albumId, @Nullable String dateTakenMs)1900 private Uri buildNotificationUri( 1901 @NonNull @OperationType int op, 1902 @Nullable String albumId, 1903 @Nullable String dateTakenMs) { 1904 1905 Objects.requireNonNull( 1906 dateTakenMs, "Cannot notify subscribers without a date taken timestamp."); 1907 1908 // base: content://media/picker_internal/ 1909 Uri.Builder builder = PICKER_INTERNAL_URI.buildUpon().appendPath(UPDATE); 1910 1911 switch (op) { 1912 case OPERATION_ADD_MEDIA: 1913 // content://media/picker_internal/update/media 1914 builder.appendPath(MEDIA); 1915 break; 1916 case OPERATION_ADD_ALBUM: 1917 // content://media/picker_internal/update/album_content/${albumId} 1918 builder.appendPath(ALBUM_CONTENT); 1919 builder.appendPath(albumId); 1920 break; 1921 case OPERATION_REMOVE_MEDIA: 1922 if (albumId != null) { 1923 // content://media/picker_internal/update/album_content/${albumId} 1924 builder.appendPath(ALBUM_CONTENT); 1925 builder.appendPath(albumId); 1926 } else { 1927 // content://media/picker_internal/update/media 1928 builder.appendPath(MEDIA); 1929 } 1930 break; 1931 default: 1932 Log.w( 1933 TAG, 1934 String.format( 1935 "Requested operation (%d) is not supported for notifications.", 1936 op)); 1937 return null; 1938 } 1939 1940 builder.appendPath(dateTakenMs); 1941 return builder.build(); 1942 } 1943 1944 /** 1945 * Get the default {@link CloudProviderInfo} at {@link PickerSyncController} construction 1946 */ 1947 @VisibleForTesting getDefaultCloudProviderInfo(@ullable String lastProvider)1948 CloudProviderInfo getDefaultCloudProviderInfo(@Nullable String lastProvider) { 1949 final List<CloudProviderInfo> providers = getAvailableCloudProviders(); 1950 1951 if (providers.size() == 1) { 1952 Log.i(TAG, "Only 1 cloud provider found, hence " + providers.get(0).authority 1953 + " is the default"); 1954 return providers.get(0); 1955 } else { 1956 Log.i(TAG, "Found " + providers.size() + " available Cloud Media Providers."); 1957 } 1958 1959 if (lastProvider != null) { 1960 for (CloudProviderInfo provider : providers) { 1961 if (Objects.equals(provider.authority, lastProvider)) { 1962 return provider; 1963 } 1964 } 1965 } 1966 1967 final String defaultProviderPkg = mConfigStore.getDefaultCloudProviderPackage(); 1968 if (defaultProviderPkg != null) { 1969 Log.i(TAG, "Default Cloud-Media-Provider package is " + defaultProviderPkg); 1970 1971 for (CloudProviderInfo provider : providers) { 1972 if (provider.matches(defaultProviderPkg)) { 1973 return provider; 1974 } 1975 } 1976 } else { 1977 Log.i(TAG, "Default Cloud-Media-Provider is not set."); 1978 } 1979 1980 // No default set or default not installed 1981 return CloudProviderInfo.EMPTY; 1982 } 1983 traceSectionName(@onNull String method)1984 private static String traceSectionName(@NonNull String method) { 1985 return "PSC." + method; 1986 } 1987 traceSectionName(@onNull String method, boolean isLocal)1988 private static String traceSectionName(@NonNull String method, boolean isLocal) { 1989 return traceSectionName(method) 1990 + "[" + (isLocal ? "local" : "cloud") + ']'; 1991 } 1992 validateCursor(Cursor cursor, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Set<String> usedPageTokens)1993 private static String validateCursor(Cursor cursor, String expectedMediaCollectionId, 1994 List<String> expectedHonoredArgs, Set<String> usedPageTokens) { 1995 final Bundle bundle = cursor.getExtras(); 1996 1997 if (bundle == null) { 1998 throw new IllegalStateException("Unable to verify the media collection id"); 1999 } 2000 2001 final String mediaCollectionId = bundle.getString(EXTRA_MEDIA_COLLECTION_ID); 2002 final String pageToken = bundle.getString(EXTRA_PAGE_TOKEN); 2003 List<String> honoredArgs = bundle.getStringArrayList(EXTRA_HONORED_ARGS); 2004 if (honoredArgs == null) { 2005 honoredArgs = new ArrayList<>(); 2006 } 2007 2008 if (expectedMediaCollectionId != null 2009 && !expectedMediaCollectionId.equals(mediaCollectionId)) { 2010 throw new IllegalStateException("Mismatched media collection id. Expected: " 2011 + expectedMediaCollectionId + ". Found: " + mediaCollectionId); 2012 } 2013 2014 if (!honoredArgs.containsAll(expectedHonoredArgs)) { 2015 throw new IllegalStateException("Unspecified honored args. Expected: " 2016 + Arrays.toString(expectedHonoredArgs.toArray()) 2017 + ". Found: " + Arrays.toString(honoredArgs.toArray())); 2018 } 2019 2020 if (usedPageTokens.contains(pageToken)) { 2021 throw new IllegalStateException("Found repeated page token: " + pageToken); 2022 } else { 2023 usedPageTokens.add(pageToken); 2024 } 2025 2026 return pageToken; 2027 } 2028 2029 private static class SyncRequestParams { 2030 static final SyncRequestParams SYNC_REQUEST_NONE = new SyncRequestParams(SYNC_TYPE_NONE); 2031 static final SyncRequestParams SYNC_REQUEST_MEDIA_RESET = 2032 new SyncRequestParams(SYNC_TYPE_MEDIA_RESET); 2033 2034 final int syncType; 2035 // Only valid for SYNC_TYPE_INCREMENTAL 2036 final long syncGeneration; 2037 // Only valid for SYNC_TYPE_[INCREMENTAL|FULL] 2038 final Bundle latestMediaCollectionInfo; 2039 // Only valid for sync triggered by opening photopicker activity. 2040 // Not valid for proactive syncs. 2041 final int mPageSize; 2042 SyncRequestParams(@yncType int syncType)2043 SyncRequestParams(@SyncType int syncType) { 2044 this(syncType, /* syncGeneration */ 0, /* latestMediaCollectionInfo */ null, 2045 /*pageSize */ PAGE_SIZE); 2046 } 2047 SyncRequestParams(@yncType int syncType, long syncGeneration, Bundle latestMediaCollectionInfo, int pageSize)2048 SyncRequestParams(@SyncType int syncType, long syncGeneration, 2049 Bundle latestMediaCollectionInfo, int pageSize) { 2050 this.syncType = syncType; 2051 this.syncGeneration = syncGeneration; 2052 this.latestMediaCollectionInfo = latestMediaCollectionInfo; 2053 this.mPageSize = pageSize; 2054 } 2055 getMediaCollectionId()2056 String getMediaCollectionId() { 2057 return latestMediaCollectionInfo.getString(MEDIA_COLLECTION_ID); 2058 } 2059 forNone()2060 static SyncRequestParams forNone() { 2061 return SYNC_REQUEST_NONE; 2062 } 2063 forResetMedia()2064 static SyncRequestParams forResetMedia() { 2065 return SYNC_REQUEST_MEDIA_RESET; 2066 } 2067 forFullMediaWithReset(@onNull Bundle latestMediaCollectionInfo)2068 static SyncRequestParams forFullMediaWithReset(@NonNull Bundle latestMediaCollectionInfo) { 2069 return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL_WITH_RESET, /* generation */ 0, 2070 latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE); 2071 } 2072 forFullMedia(@onNull Bundle latestMediaCollectionInfo)2073 static SyncRequestParams forFullMedia(@NonNull Bundle latestMediaCollectionInfo) { 2074 return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL, /* generation */ 0, 2075 latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE); 2076 } 2077 forIncremental(long generation, Bundle latestMediaCollectionInfo)2078 static SyncRequestParams forIncremental(long generation, Bundle latestMediaCollectionInfo) { 2079 return new SyncRequestParams(SYNC_TYPE_MEDIA_INCREMENTAL, generation, 2080 latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE); 2081 } 2082 2083 @Override toString()2084 public String toString() { 2085 return "SyncRequestParams{type=" + syncTypeToString(syncType) 2086 + ", gen=" + syncGeneration + ", latest=" + latestMediaCollectionInfo 2087 + ", pageSize=" + mPageSize + '}'; 2088 } 2089 } 2090 syncTypeToString(@yncType int syncType)2091 private static String syncTypeToString(@SyncType int syncType) { 2092 switch (syncType) { 2093 case SYNC_TYPE_NONE: 2094 return "NONE"; 2095 case SYNC_TYPE_MEDIA_INCREMENTAL: 2096 return "MEDIA_INCREMENTAL"; 2097 case SYNC_TYPE_MEDIA_FULL: 2098 return "MEDIA_FULL"; 2099 case SYNC_TYPE_MEDIA_RESET: 2100 return "MEDIA_RESET"; 2101 case SYNC_TYPE_MEDIA_FULL_WITH_RESET: 2102 return "MEDIA_FULL_WITH_RESET"; 2103 default: 2104 return "Unknown"; 2105 } 2106 } 2107 isCloudProviderUnset(@ullable String lastProviderAuthority)2108 private static boolean isCloudProviderUnset(@Nullable String lastProviderAuthority) { 2109 return Objects.equals(lastProviderAuthority, PREFS_VALUE_CLOUD_PROVIDER_UNSET); 2110 } 2111 2112 /** 2113 * Print the {@link PickerSyncController} state into the given stream. 2114 */ dump(PrintWriter writer)2115 public void dump(PrintWriter writer) { 2116 writer.println("Picker sync controller state:"); 2117 2118 writer.println(" mLocalProvider=" + getLocalProvider()); 2119 writer.println(" mCloudProviderInfo=" + getCurrentCloudProviderInfo()); 2120 writer.println(" allAvailableCloudProviders=" 2121 + CloudProviderUtils.getAllAvailableCloudProviders(mContext, mConfigStore)); 2122 2123 writer.println(" cachedAuthority=" 2124 + mUserPrefs.getString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, /* defValue */ null)); 2125 writer.println(" cachedLocalMediaCollectionInfo=" 2126 + getCachedMediaCollectionInfo(/* isLocal */ true)); 2127 writer.println(" cachedCloudMediaCollectionInfo=" 2128 + getCachedMediaCollectionInfo(/* isLocal */ false)); 2129 } 2130 2131 /** 2132 * Returns the associated Picker DB instance. 2133 */ getDbFacade()2134 public PickerDbFacade getDbFacade() { 2135 return mDbFacade; 2136 } 2137 2138 /** 2139 * Returns true when all the following conditions are true: 2140 * 1. Current cloud provider is not null. 2141 * 2. Current cloud provider is present in the given providers list. 2142 * 3. Database has currently enabled cloud provider queries. 2143 * 4. The given provider is equal to the current provider. 2144 */ shouldQueryCloudMedia( @onNull List<String> providers, @Nullable String cloudProvider)2145 public boolean shouldQueryCloudMedia( 2146 @NonNull List<String> providers, 2147 @Nullable String cloudProvider) { 2148 return cloudProvider != null 2149 && providers.contains(cloudProvider) 2150 && shouldQueryCloudMedia(cloudProvider); 2151 } 2152 2153 /** 2154 * Returns true when all the following conditions are true: 2155 * 1. Current cloud provider is not null. 2156 * 2. Database has currently enabled cloud provider queries. 2157 */ shouldQueryCloudMedia( @ullable String cloudProvider)2158 public boolean shouldQueryCloudMedia( 2159 @Nullable String cloudProvider) { 2160 try (CloseableReentrantLock ignored = 2161 mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 2162 return cloudProvider != null 2163 && cloudProvider.equals(getCloudProviderWithTimeout()) 2164 && cloudProvider.equals(mDbFacade.getCloudProvider()); 2165 } catch (UnableToAcquireLockException e) { 2166 Log.e(TAG, "Could not check if cloud media should be queried", e); 2167 return false; 2168 } 2169 } 2170 2171 /** 2172 * Returns true when all the following conditions are true: 2173 * 1. Input cloud provider is not null. 2174 * 2. Input cloud provider is present in the given providers list. 2175 * 3. Input cloud provider is also the current cloud provider. 2176 * 4. Search feature is enabled for the given cloud provider. 2177 * Otherwise returns false. 2178 */ shouldQueryCloudMediaForSearch( @onNull Set<String> providers, @Nullable String cloudProvider)2179 public boolean shouldQueryCloudMediaForSearch( 2180 @NonNull Set<String> providers, 2181 @Nullable String cloudProvider) { 2182 try (CloseableReentrantLock ignored = 2183 mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 2184 return cloudProvider != null 2185 && providers.contains(cloudProvider) 2186 && cloudProvider.equals(getCloudProviderWithTimeout()) 2187 && getSearchState().isCloudSearchEnabled(mContext, cloudProvider); 2188 } catch (UnableToAcquireLockException e) { 2189 Log.e(TAG, "Could not check if cloud media should be queried", e); 2190 return false; 2191 } 2192 } 2193 2194 /** 2195 * Returns true when all the following conditions are true: 2196 * 1. Current local provider is not null. 2197 * 2. Current local provider is present in the given providers list. 2198 * 3. Search feature is enabled for the current local provider. 2199 * Otherwise returns false. 2200 */ shouldQueryLocalMediaForSearch( @onNull Set<String> providers)2201 public boolean shouldQueryLocalMediaForSearch( 2202 @NonNull Set<String> providers) { 2203 final String localProvider = getLocalProvider(); 2204 return localProvider != null 2205 && providers.contains(localProvider) 2206 && getSearchState().isLocalSearchEnabled(); 2207 } 2208 2209 /** 2210 * @param providers List of providers for the current request 2211 * @return Returns whether the local sync is possible 2212 * Returns true if all of the following are true: 2213 * -The retrieved local provider is not null 2214 * -The input list of providers contains the current provider 2215 * -The CMP implements categories API 2216 * Otherwise, we get false 2217 */ shouldQueryLocalMediaSets(@onNull Set<String> providers)2218 public boolean shouldQueryLocalMediaSets(@NonNull Set<String> providers) { 2219 Objects.requireNonNull(providers); 2220 final String localProvider = getLocalProvider(); 2221 return localProvider != null 2222 && providers.contains(localProvider) 2223 && getCategoriesState().areCategoriesEnabled(mContext, localProvider); 2224 } 2225 2226 /** 2227 * @param providers Set of providers for the current request 2228 * @param cloudProvider The cloudAuthority to query for 2229 * @return Returns whether the local sync is possible 2230 * Returns true if all of the following are true: 2231 * -The given cloud provider is not null 2232 * -The input list of providers contains the current cloud provider 2233 * -Input cloud provider is the same as the current cloud provider 2234 * -The CMP implements categories API 2235 * Otherwise, we get false 2236 */ shouldQueryCloudMediaSets( @onNull Set<String> providers, @Nullable String cloudProvider)2237 public boolean shouldQueryCloudMediaSets( 2238 @NonNull Set<String> providers, 2239 @Nullable String cloudProvider) { 2240 Objects.requireNonNull(providers); 2241 try (CloseableReentrantLock ignored = 2242 mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 2243 return cloudProvider != null 2244 && providers.contains(cloudProvider) 2245 && cloudProvider.equals(getCloudProviderWithTimeout()) 2246 && getCategoriesState().areCategoriesEnabled(mContext, cloudProvider); 2247 } catch (UnableToAcquireLockException e) { 2248 Log.e(TAG, "Could not check if cloud media sets are to be queried", e); 2249 return false; 2250 } 2251 } 2252 2253 @NonNull getCategoriesState()2254 public CategoriesState getCategoriesState() { 2255 return mCategoriesState; 2256 } 2257 2258 /** 2259 * Disable cloud queries if the new collection id received from the cloud provider in the media 2260 * event notification is different than the cached value. 2261 */ handleMediaEventNotification(Boolean localOnly, @NonNull String authority, @Nullable String newCollectionId)2262 public void handleMediaEventNotification(Boolean localOnly, @NonNull String authority, 2263 @Nullable String newCollectionId) { 2264 if (!localOnly && newCollectionId != null) { 2265 try (CloseableReentrantLock ignored = mPickerSyncLockManager 2266 .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) { 2267 final String currentCloudProvider = getCloudProviderWithTimeout(); 2268 if (authority.equals(currentCloudProvider) && !newCollectionId 2269 .equals(mLatestCloudProviderCollectionInfo.getCollectionId())) { 2270 disablePickerCloudMediaQueries(/* isLocal */ false); 2271 } 2272 } catch (UnableToAcquireLockException e) { 2273 Log.e(TAG, "Could not handle media event notification", e); 2274 } 2275 } 2276 } 2277 2278 /** 2279 * Executes a sync for grants from the external database to the picker database. 2280 * 2281 * This should only be called when the picker is in MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP 2282 * action. It requires a valid packageUid and mimeTypes with which the picker was invoked to 2283 * ensure that the sync only happens for the items that: 2284 * <li>match the mimeTypes</li> 2285 * <li>are granted to the package and userId corresponding to the provided packageUid</li> 2286 * 2287 * It fetches the rows from media_grants table in the external.db that matches the criteria and 2288 * inserts them in the media_grants table in picker.db 2289 */ executeGrantsSync( boolean shouldSyncGrants, int packageUid, String[] mimeTypes)2290 public void executeGrantsSync( 2291 boolean shouldSyncGrants, int packageUid, 2292 String[] mimeTypes) { 2293 // empty the grants table. 2294 executeClearAllGrants(packageUid); 2295 2296 // sync all grants into the table 2297 if (shouldSyncGrants) { 2298 final ContentResolver resolver = mContext.getContentResolver(); 2299 try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { 2300 assert client != null; 2301 final Bundle extras = new Bundle(); 2302 extras.putInt(Intent.EXTRA_UID, packageUid); 2303 extras.putStringArray(EXTRA_MEDIA_GRANTS_MIME_TYPES, mimeTypes); 2304 try (Cursor c = client.query(Uri.parse(MEDIA_GRANTS_URI_PATH), 2305 /* projection= */ null, 2306 /* queryArgs= */ extras, 2307 null)) { 2308 Trace.beginSection(traceSectionName( 2309 "executeGrantsSync", /* isLocal */ true)); 2310 try (PickerDbFacade.DbWriteOperation operation = 2311 mDbFacade.beginInsertGrantsOperation()) { 2312 int grantsInsertedCount = operation.execute(c); 2313 operation.setSuccess(); 2314 Log.i(TAG, "Successfully executed grants sync operation operation." 2315 + " Result count: " + grantsInsertedCount); 2316 } finally { 2317 Trace.endSection(); 2318 } 2319 } 2320 } catch (RemoteException e) { 2321 Log.e(TAG, "Remote exception received while fetching grants. " + e.getMessage()); 2322 } 2323 } 2324 } 2325 2326 /** 2327 * Before a sync for grants is initiated, this method is used to clear any stale grants that 2328 * exists in the database. 2329 * 2330 * This should only be called when the picker is in MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP 2331 * action. It requires a valid packageUid with which the picker was invoked to 2332 * ensure that all the rows that represents the items granted to the package and userId 2333 * corresponding to the provided packageUid are cleared from the media_grants table in picker.db 2334 */ executeClearAllGrants(int packageUid)2335 private void executeClearAllGrants(int packageUid) { 2336 Trace.beginSection(traceSectionName("executeClearAllGrants", /* isLocal */ true)); 2337 int userId = uidToUserId(packageUid); 2338 String[] packageNames = getPackageNameFromUid(mContext, packageUid); 2339 2340 try (PickerDbFacade.DbWriteOperation operation = 2341 mDbFacade.beginClearGrantsOperation(packageNames, userId)) { 2342 final int clearedGrantsCount = operation.execute(/* cursor */ null); 2343 operation.setSuccess(); 2344 2345 Log.i(TAG, "Successfully executed clear grants operation." 2346 + " Result count: " + clearedGrantsCount); 2347 } catch (SQLException e) { 2348 Log.e(TAG, "Unable to clear grants for this session: " + e.getMessage()); 2349 } finally { 2350 Trace.endSection(); 2351 } 2352 } 2353 2354 /** 2355 * Returns an Array of packageNames corresponding to the input package uid. 2356 */ getPackageNameFromUid(Context context, int callingPackageUid)2357 public static String[] getPackageNameFromUid(Context context, int callingPackageUid) { 2358 final PackageManager pm = context.getPackageManager(); 2359 return pm.getPackagesForUid(callingPackageUid); 2360 } 2361 2362 /** 2363 * Generates and returns userId from the input package uid. 2364 */ uidToUserId(int uid)2365 public static int uidToUserId(int uid) { 2366 // Get the userId from packageUid as the initiator could be a cloned app, which 2367 // accesses Media via MP of its parent user and Binder's callingUid reflects 2368 // the latter. 2369 return uid / PER_USER_RANGE; 2370 } 2371 } 2372