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