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_TOKEN; 23 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION; 24 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION; 25 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID; 26 27 import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri; 28 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri; 29 import static com.android.providers.media.PickerUriResolver.getMediaUri; 30 31 import android.annotation.IntDef; 32 import android.content.Context; 33 import android.content.SharedPreferences; 34 import android.content.pm.PackageManager; 35 import android.database.Cursor; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.Process; 40 import android.os.Trace; 41 import android.os.storage.StorageManager; 42 import android.provider.CloudMediaProvider; 43 import android.provider.CloudMediaProviderContract; 44 import android.text.TextUtils; 45 import android.util.ArraySet; 46 import android.util.Log; 47 import android.widget.Toast; 48 49 import androidx.annotation.GuardedBy; 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.VisibleForTesting; 53 54 import com.android.modules.utils.BackgroundThread; 55 import com.android.modules.utils.build.SdkLevel; 56 import com.android.providers.media.ConfigStore; 57 import com.android.providers.media.R; 58 import com.android.providers.media.photopicker.data.CloudProviderInfo; 59 import com.android.providers.media.photopicker.data.PickerDbFacade; 60 import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger; 61 import com.android.providers.media.photopicker.util.CloudProviderUtils; 62 import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException; 63 import com.android.providers.media.util.ForegroundThread; 64 65 import java.lang.annotation.Retention; 66 import java.lang.annotation.RetentionPolicy; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.List; 70 import java.util.Objects; 71 import java.util.Set; 72 import java.util.concurrent.locks.ReentrantLock; 73 74 /** 75 * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances on the device 76 * into the picker db. 77 */ 78 public class PickerSyncController { 79 80 public static final ReentrantLock sIdleMaintenanceSyncLock = new ReentrantLock(); 81 private static final String TAG = "PickerSyncController"; 82 private static final boolean DEBUG = false; 83 84 private static final String PREFS_KEY_CLOUD_PROVIDER_AUTHORITY = "cloud_provider_authority"; 85 private static final String PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION = 86 "cloud_provider_pending_notification"; 87 private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:"; 88 private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:"; 89 90 private static final String PICKER_USER_PREFS_FILE_NAME = "picker_user_prefs"; 91 public static final String PICKER_SYNC_PREFS_FILE_NAME = "picker_sync_prefs"; 92 public static final String LOCAL_PICKER_PROVIDER_AUTHORITY = 93 "com.android.providers.media.photopicker"; 94 95 private static final String PREFS_VALUE_CLOUD_PROVIDER_UNSET = "-"; 96 97 private static final int SYNC_TYPE_NONE = 0; 98 private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1; 99 private static final int SYNC_TYPE_MEDIA_FULL = 2; 100 private static final int SYNC_TYPE_MEDIA_RESET = 3; 101 @NonNull 102 private static final Handler sBgThreadHandler = BackgroundThread.getHandler(); 103 @IntDef(flag = false, prefix = { "SYNC_TYPE_" }, value = { 104 SYNC_TYPE_NONE, 105 SYNC_TYPE_MEDIA_INCREMENTAL, 106 SYNC_TYPE_MEDIA_FULL, 107 SYNC_TYPE_MEDIA_RESET, 108 }) 109 @Retention(RetentionPolicy.SOURCE) 110 private @interface SyncType {} 111 112 private final Context mContext; 113 private final ConfigStore mConfigStore; 114 private final PickerDbFacade mDbFacade; 115 private final SharedPreferences mSyncPrefs; 116 private final SharedPreferences mUserPrefs; 117 private final String mLocalProvider; 118 private final long mSyncDelayMs; 119 private final Runnable mSyncAllMediaCallback; 120 121 private final PhotoPickerUiEventLogger mLogger; 122 private final Object mCloudSyncLock = new Object(); 123 // TODO(b/278562157): If there is a dependency on the sync process, always acquire the 124 // {@link mCloudSyncLock} before {@link mCloudProviderLock} to avoid deadlock. 125 private final Object mCloudProviderLock = new Object(); 126 @GuardedBy("mCloudProviderLock") 127 private CloudProviderInfo mCloudProviderInfo; 128 PickerSyncController(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore)129 public PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade, 130 @NonNull ConfigStore configStore) { 131 this(context, dbFacade, configStore, LOCAL_PICKER_PROVIDER_AUTHORITY); 132 } 133 134 @VisibleForTesting PickerSyncController(@onNull Context context, @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull String localProvider)135 public PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade, 136 @NonNull ConfigStore configStore, @NonNull String localProvider) { 137 mContext = context; 138 mConfigStore = configStore; 139 mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME, 140 Context.MODE_PRIVATE); 141 mUserPrefs = mContext.getSharedPreferences(PICKER_USER_PREFS_FILE_NAME, 142 Context.MODE_PRIVATE); 143 mDbFacade = dbFacade; 144 mLocalProvider = localProvider; 145 mSyncAllMediaCallback = this::syncAllMedia; 146 mLogger = new PhotoPickerUiEventLogger(); 147 mSyncDelayMs = configStore.getPickerSyncDelayMs(); 148 149 initCloudProvider(); 150 } 151 initCloudProvider()152 private void initCloudProvider() { 153 synchronized (mCloudProviderLock) { 154 if (!mConfigStore.isCloudMediaInPhotoPickerEnabled()) { 155 Log.d(TAG, "Cloud-Media-in-Photo-Picker feature is disabled during " + TAG 156 + " construction."); 157 persistCloudProviderInfo(CloudProviderInfo.EMPTY, /* shouldUnset */ false); 158 return; 159 } 160 161 final String cachedAuthority = mUserPrefs.getString( 162 PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, null); 163 164 if (isCloudProviderUnset(cachedAuthority)) { 165 Log.d(TAG, "Cloud provider state is unset during " + TAG + " construction."); 166 setCurrentCloudProviderInfo(CloudProviderInfo.EMPTY); 167 return; 168 } 169 170 initCloudProviderLocked(cachedAuthority); 171 } 172 } 173 initCloudProviderLocked(@ullable String cachedAuthority)174 private void initCloudProviderLocked(@Nullable String cachedAuthority) { 175 final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority); 176 177 if (Objects.equals(defaultInfo.authority, cachedAuthority)) { 178 // Just set it without persisting since it's not changing and persisting would 179 // notify the user that cloud media is now available 180 setCurrentCloudProviderInfo(defaultInfo); 181 } else { 182 // Persist it so that we notify the user that cloud media is now available 183 persistCloudProviderInfo(defaultInfo, /* shouldUnset */ false); 184 } 185 186 Log.d(TAG, "Initialized cloud provider to: " + defaultInfo.authority); 187 } 188 189 /** 190 * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances 191 */ syncAllMedia()192 public void syncAllMedia() { 193 Log.d(TAG, "syncAllMedia"); 194 195 Trace.beginSection(traceSectionName("syncAllMedia")); 196 try { 197 syncAllMediaFromLocalProvider(); 198 syncAllMediaFromCloudProvider(); 199 } finally { 200 Trace.endSection(); 201 } 202 } 203 204 205 /** 206 * Syncs the local media 207 */ syncAllMediaFromLocalProvider()208 public void syncAllMediaFromLocalProvider() { 209 // Picker sync and special format update can execute concurrently and run into a deadlock. 210 // Acquiring a lock before execution of each flow to avoid this. 211 sIdleMaintenanceSyncLock.lock(); 212 try { 213 syncAllMediaFromProvider(mLocalProvider, /* isLocal */ true, /* retryOnFailure */ true); 214 } finally { 215 sIdleMaintenanceSyncLock.unlock(); 216 } 217 } 218 syncAllMediaFromCloudProvider()219 private void syncAllMediaFromCloudProvider() { 220 synchronized (mCloudSyncLock) { 221 final String cloudProvider = getCloudProvider(); 222 223 // Disable cloud queries in the database. If any cloud related queries come through 224 // while cloud sync is in progress, all cloud items will be ignored and local items will 225 // be returned. 226 mDbFacade.setCloudProvider(null); 227 228 // Trigger a sync. 229 final boolean isSyncCommitted = syncAllMediaFromProvider(cloudProvider, 230 /* isLocal */ false, /* retryOnFailure */ true); 231 232 // Check if sync was committed i.e. the latest collection info was persisted. 233 if (!isSyncCommitted) { 234 Log.e(TAG, "Failed to sync with cloud provider - " + cloudProvider 235 + ". The cloud provider may have changed during the sync"); 236 return; 237 } 238 239 // Reset the album_media table every time we sync all media 240 // TODO(258765155): do we really need to reset for both providers? 241 resetAlbumMedia(); 242 243 // Re-enable cloud queries in the database for the latest cloud provider. 244 synchronized (mCloudProviderLock) { 245 if (Objects.equals(mCloudProviderInfo.authority, cloudProvider)) { 246 mDbFacade.setCloudProvider(cloudProvider); 247 } else { 248 Log.e(TAG, "Failed to sync with cloud provider - " + cloudProvider 249 + ". The cloud provider has changed to " 250 + mCloudProviderInfo.authority); 251 } 252 } 253 } 254 } 255 256 /** 257 * Syncs album media from the local and currently enabled cloud {@link CloudMediaProvider} 258 * instances 259 */ syncAlbumMedia(String albumId, boolean isLocal)260 public void syncAlbumMedia(String albumId, boolean isLocal) { 261 if (isLocal) { 262 syncAlbumMediaFromLocalProvider(albumId); 263 } else { 264 syncAlbumMediaFromCloudProvider(albumId); 265 } 266 } 267 syncAlbumMediaFromLocalProvider(@onNull String albumId)268 private void syncAlbumMediaFromLocalProvider(@NonNull String albumId) { 269 syncAlbumMediaFromProvider(mLocalProvider, /* isLocal */ true, albumId); 270 } 271 syncAlbumMediaFromCloudProvider(@onNull String albumId)272 private void syncAlbumMediaFromCloudProvider(@NonNull String albumId) { 273 synchronized (mCloudSyncLock) { 274 syncAlbumMediaFromProvider(getCloudProvider(), /* isLocal */ false, albumId); 275 } 276 } 277 resetAlbumMedia()278 private void resetAlbumMedia() { 279 executeSyncAlbumReset(mLocalProvider, /* isLocal */ true, /* albumId */ null); 280 281 synchronized (mCloudSyncLock) { 282 executeSyncAlbumReset(getCloudProvider(), /* isLocal */ false, /* albumId */ null); 283 } 284 } 285 286 /** 287 * Resets media library previously synced from the current {@link CloudMediaProvider} as well 288 * as the {@link #mLocalProvider local provider}. 289 */ resetAllMedia()290 public void resetAllMedia() { 291 resetAllMedia(mLocalProvider, /* isLocal */ true); 292 synchronized (mCloudSyncLock) { 293 resetAllMedia(getCloudProvider(), /* isLocal */ false); 294 } 295 } 296 resetAllMedia(@ullable String authority, boolean isLocal)297 private boolean resetAllMedia(@Nullable String authority, boolean isLocal) { 298 Trace.beginSection(traceSectionName("resetAllMedia", isLocal)); 299 try { 300 executeSyncReset(authority, isLocal); 301 return resetCachedMediaCollectionInfo(authority, isLocal); 302 } finally { 303 Trace.endSection(); 304 } 305 } 306 307 @NonNull getCloudProviderInfo(String authority, boolean ignoreAllowlist)308 private CloudProviderInfo getCloudProviderInfo(String authority, boolean ignoreAllowlist) { 309 if (authority == null) { 310 return CloudProviderInfo.EMPTY; 311 } 312 313 final List<CloudProviderInfo> availableProviders = ignoreAllowlist 314 ? CloudProviderUtils.getAllAvailableCloudProviders(mContext, mConfigStore) 315 : CloudProviderUtils.getAvailableCloudProviders(mContext, mConfigStore); 316 317 for (CloudProviderInfo info : availableProviders) { 318 if (Objects.equals(info.authority, authority)) { 319 return info; 320 } 321 } 322 323 return CloudProviderInfo.EMPTY; 324 } 325 326 /** 327 * @return list of available <b>and</b> allowlisted {@link CloudMediaProvider}-s. 328 */ 329 @VisibleForTesting getAvailableCloudProviders()330 List<CloudProviderInfo> getAvailableCloudProviders() { 331 return CloudProviderUtils.getAvailableCloudProviders(mContext, mConfigStore); 332 } 333 334 /** 335 * Enables a provider with {@code authority} as the default cloud {@link CloudMediaProvider}. 336 * If {@code authority} is set to {@code null}, it simply clears the cloud provider. 337 * 338 * Note, that this doesn't sync the new provider after switching, however, no cloud items will 339 * be available from the picker db until the next sync. Callers should schedule a sync in the 340 * background after switching providers. 341 * 342 * @return {@code true} if the provider was successfully enabled or cleared, {@code false} 343 * otherwise. 344 */ setCloudProvider(@ullable String authority)345 public boolean setCloudProvider(@Nullable String authority) { 346 Trace.beginSection(traceSectionName("setCloudProvider")); 347 try { 348 return setCloudProviderInternal(authority, /* ignoreAllowlist */ false); 349 } finally { 350 Trace.endSection(); 351 } 352 } 353 354 /** 355 * Set cloud provider ignoring allowlist. 356 * 357 * @return {@code true} if the provider was successfully enabled or cleared, {@code false} 358 * otherwise. 359 */ forceSetCloudProvider(@ullable String authority)360 public boolean forceSetCloudProvider(@Nullable String authority) { 361 Trace.beginSection(traceSectionName("forceSetCloudProvider")); 362 try { 363 return setCloudProviderInternal(authority, /* ignoreAllowlist */ true); 364 } finally { 365 Trace.endSection(); 366 } 367 } 368 setCloudProviderInternal(@ullable String authority, boolean ignoreAllowList)369 private boolean setCloudProviderInternal(@Nullable String authority, boolean ignoreAllowList) { 370 Log.d(TAG, "setCloudProviderInternal() auth=" + authority + ", " 371 + "ignoreAllowList=" + ignoreAllowList); 372 if (DEBUG) { 373 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 374 } 375 376 if (!mConfigStore.isCloudMediaInPhotoPickerEnabled()) { 377 Log.w(TAG, "Ignoring a request to set the CloudMediaProvider (" + authority + ") " 378 + "since the Cloud-Media-in-Photo-Picker feature is disabled"); 379 return false; 380 } 381 382 synchronized (mCloudProviderLock) { 383 if (Objects.equals(mCloudProviderInfo.authority, authority)) { 384 Log.w(TAG, "Cloud provider already set: " + authority); 385 return true; 386 } 387 } 388 389 final CloudProviderInfo newProviderInfo = getCloudProviderInfo(authority, ignoreAllowList); 390 if (authority == null || !newProviderInfo.isEmpty()) { 391 synchronized (mCloudProviderLock) { 392 // Disable cloud provider queries on the db until next sync 393 // This will temporarily *clear* the cloud provider on the db facade and prevent 394 // any queries from seeing cloud media until a sync where the cloud provider will be 395 // reset on the facade 396 mDbFacade.setCloudProvider(null); 397 398 final String oldAuthority = mCloudProviderInfo.authority; 399 persistCloudProviderInfo(newProviderInfo, /* shouldUnset */ true); 400 401 // TODO(b/242897322): Log from PickerViewModel using its InstanceId when relevant 402 mLogger.logPickerCloudProviderChanged(newProviderInfo.uid, 403 newProviderInfo.packageName); 404 Log.i(TAG, "Cloud provider changed successfully. Old: " 405 + oldAuthority + ". New: " + newProviderInfo.authority); 406 } 407 408 return true; 409 } 410 411 Log.w(TAG, "Cloud provider not supported: " + authority); 412 return false; 413 } 414 415 /** 416 * @return {@link CloudProviderInfo} for the current {@link CloudMediaProvider} or 417 * {@link CloudProviderInfo#EMPTY} if the {@link CloudMediaProvider} integration is not 418 * enabled. 419 */ 420 @NonNull getCurrentCloudProviderInfo()421 public CloudProviderInfo getCurrentCloudProviderInfo() { 422 synchronized (mCloudProviderLock) { 423 return mCloudProviderInfo; 424 } 425 } 426 427 /** 428 * Set {@link PickerSyncController#mCloudProviderInfo} as the current {@link CloudMediaProvider} 429 * or {@link CloudProviderInfo#EMPTY} if the {@link CloudMediaProvider} integration 430 * disabled by the user. 431 */ setCurrentCloudProviderInfo(@onNull CloudProviderInfo cloudProviderInfo)432 private void setCurrentCloudProviderInfo(@NonNull CloudProviderInfo cloudProviderInfo) { 433 synchronized (mCloudProviderLock) { 434 mCloudProviderInfo = cloudProviderInfo; 435 } 436 } 437 438 /** 439 * @return {@link android.content.pm.ProviderInfo#authority authority} of the current 440 * {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider} 441 * integration is not enabled. 442 */ 443 @Nullable getCloudProvider()444 public String getCloudProvider() { 445 synchronized (mCloudProviderLock) { 446 return mCloudProviderInfo.authority; 447 } 448 } 449 450 /** 451 * @return {@link android.content.pm.ProviderInfo#authority authority} of the local provider. 452 */ 453 @NonNull getLocalProvider()454 public String getLocalProvider() { 455 return mLocalProvider; 456 } 457 isProviderEnabled(String authority)458 public boolean isProviderEnabled(String authority) { 459 if (mLocalProvider.equals(authority)) { 460 return true; 461 } 462 463 synchronized (mCloudProviderLock) { 464 if (!mCloudProviderInfo.isEmpty() 465 && Objects.equals(mCloudProviderInfo.authority, authority)) { 466 return true; 467 } 468 } 469 470 return false; 471 } 472 isProviderEnabled(String authority, int uid)473 public boolean isProviderEnabled(String authority, int uid) { 474 if (uid == Process.myUid() && mLocalProvider.equals(authority)) { 475 return true; 476 } 477 478 synchronized (mCloudProviderLock) { 479 if (!mCloudProviderInfo.isEmpty() && uid == mCloudProviderInfo.uid 480 && Objects.equals(mCloudProviderInfo.authority, authority)) { 481 return true; 482 } 483 } 484 485 return false; 486 } 487 isProviderSupported(String authority, int uid)488 public boolean isProviderSupported(String authority, int uid) { 489 if (uid == Process.myUid() && mLocalProvider.equals(authority)) { 490 return true; 491 } 492 493 // TODO(b/232738117): Enforce allow list here. This works around some CTS failure late in 494 // Android T. The current implementation is fine since cloud providers is only supported 495 // for app developers testing. 496 final List<CloudProviderInfo> infos = 497 CloudProviderUtils.getAllAvailableCloudProviders(mContext, mConfigStore); 498 for (CloudProviderInfo info : infos) { 499 if (info.uid == uid && Objects.equals(info.authority, authority)) { 500 return true; 501 } 502 } 503 504 return false; 505 } 506 507 /** 508 * Notifies about media events like inserts/updates/deletes from cloud and local providers and 509 * syncs the changes in the background. 510 * 511 * There is a delay before executing the background sync to artificially throttle the burst 512 * notifications. 513 */ notifyMediaEvent()514 public void notifyMediaEvent() { 515 sBgThreadHandler.removeCallbacks(mSyncAllMediaCallback); 516 sBgThreadHandler.postDelayed(mSyncAllMediaCallback, mSyncDelayMs); 517 } 518 519 /** 520 * Notifies about package removal 521 */ notifyPackageRemoval(String packageName)522 public void notifyPackageRemoval(String packageName) { 523 synchronized (mCloudProviderLock) { 524 if (mCloudProviderInfo.matches(packageName)) { 525 Log.i(TAG, "Package " + packageName 526 + " is the current cloud provider and got removed"); 527 resetCloudProvider(); 528 } 529 } 530 } 531 resetCloudProvider()532 private void resetCloudProvider() { 533 synchronized (mCloudProviderLock) { 534 setCloudProvider(/* authority */ null); 535 536 /** 537 * {@link #setCloudProvider(String null)} sets the cloud provider state to UNSET. 538 * Clearing the persisted cloud provider authority to set the state as NOT_SET instead. 539 */ 540 clearPersistedCloudProviderAuthority(); 541 542 initCloudProviderLocked(/* cachedAuthority */ null); 543 } 544 } 545 546 // TODO(b/257887919): Build proper UI and remove this. 547 /** 548 * Notifies about picker UI launched 549 */ notifyPickerLaunch()550 public void notifyPickerLaunch() { 551 final String authority = getCloudProvider(); 552 553 final boolean hasPendingNotification = mUserPrefs.getBoolean( 554 PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, /* defaultValue */ false); 555 556 if (!hasPendingNotification || (authority == null)) { 557 Log.d(TAG, "No pending UI notification"); 558 return; 559 } 560 561 // Offload showing the UI on a fg thread to avoid the expensive binder request 562 // to fetch the app name blocking the picker launch 563 ForegroundThread.getHandler().post(() -> { 564 Log.i(TAG, "Cloud media now available in the picker"); 565 566 final PackageManager pm = mContext.getPackageManager(); 567 final String appName = CloudProviderUtils.getProviderLabel(pm, authority); 568 569 final String message = mContext.getResources().getString(R.string.picker_cloud_sync, 570 appName); 571 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 572 }); 573 574 // Clear the notification 575 updateBooleanUserPref(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, false); 576 } 577 updateBooleanUserPref(String key, boolean value)578 private void updateBooleanUserPref(String key, boolean value) { 579 final SharedPreferences.Editor editor = mUserPrefs.edit(); 580 editor.putBoolean(key, value); 581 editor.apply(); 582 } 583 syncAlbumMediaFromProvider(String authority, boolean isLocal, String albumId)584 private void syncAlbumMediaFromProvider(String authority, boolean isLocal, String albumId) { 585 final Bundle queryArgs = new Bundle(); 586 queryArgs.putString(EXTRA_ALBUM_ID, albumId); 587 588 Trace.beginSection(traceSectionName("syncAlbumMediaFromProvider", isLocal)); 589 try { 590 executeSyncAlbumReset(authority, isLocal, albumId); 591 592 if (authority != null) { 593 executeSyncAddAlbum(authority, isLocal, albumId, queryArgs); 594 } 595 } catch (RuntimeException e) { 596 // Unlike syncAllMediaFromProvider, we don't retry here because any errors would have 597 // occurred in fetching all the album_media since incremental sync is not supported. 598 // A full sync is therefore unlikely to resolve any issue 599 Log.e(TAG, "Failed to sync album media", e); 600 } finally { 601 Trace.endSection(); 602 } 603 } 604 605 /** 606 * Returns true if the sync was successful and the latest collection info was persisted. 607 */ syncAllMediaFromProvider(@ullable String authority, boolean isLocal, boolean retryOnFailure)608 private boolean syncAllMediaFromProvider(@Nullable String authority, boolean isLocal, 609 boolean retryOnFailure) { 610 Log.d(TAG, "syncAllMediaFromProvider() " + (isLocal ? "LOCAL" : "CLOUD") 611 + ", auth=" + authority 612 + ", retry=" + retryOnFailure); 613 if (DEBUG) { 614 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 615 } 616 617 Trace.beginSection(traceSectionName("syncAllMediaFromProvider", isLocal)); 618 try { 619 final SyncRequestParams params = getSyncRequestParams(authority, isLocal); 620 621 switch (params.syncType) { 622 case SYNC_TYPE_MEDIA_RESET: 623 // Can only happen when |authority| has been set to null and we need to clean up 624 return resetAllMedia(authority, isLocal); 625 case SYNC_TYPE_MEDIA_FULL: 626 if (!resetAllMedia(authority, isLocal)) { 627 return false; 628 } 629 630 // Pass a mutable empty bundle intentionally because it might be populated with 631 // the next page token as part of a query to a cloud provider supporting 632 // pagination 633 executeSyncAdd(authority, isLocal, params.getMediaCollectionId(), 634 /* isIncrementalSync */ false, /* queryArgs */ new Bundle()); 635 636 // Commit sync position 637 return cacheMediaCollectionInfo( 638 authority, isLocal, params.latestMediaCollectionInfo); 639 case SYNC_TYPE_MEDIA_INCREMENTAL: 640 final Bundle queryArgs = new Bundle(); 641 queryArgs.putLong(EXTRA_SYNC_GENERATION, params.syncGeneration); 642 643 executeSyncAdd(authority, isLocal, params.getMediaCollectionId(), 644 /* isIncrementalSync */ true, queryArgs); 645 executeSyncRemove(authority, isLocal, params.getMediaCollectionId(), queryArgs); 646 647 // Commit sync position 648 return cacheMediaCollectionInfo( 649 authority, isLocal, params.latestMediaCollectionInfo); 650 case SYNC_TYPE_NONE: 651 return true; 652 default: 653 throw new IllegalArgumentException("Unexpected sync type: " + params.syncType); 654 } 655 } catch (RequestObsoleteException e) { 656 Log.e(TAG, "Failed to sync all media because authority has changed: ", e); 657 } catch (RuntimeException e) { 658 // Reset all media for the cloud provider in case it never succeeds 659 resetAllMedia(authority, isLocal); 660 661 // Attempt a full sync. If this fails, the db table would have been reset, 662 // flushing all old content and leaving the picker UI empty. 663 Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e); 664 if (retryOnFailure) { 665 return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false); 666 } 667 } finally { 668 Trace.endSection(); 669 } 670 return false; 671 } 672 executeSyncReset(String authority, boolean isLocal)673 private void executeSyncReset(String authority, boolean isLocal) { 674 Log.i(TAG, "Executing SyncReset. isLocal: " + isLocal + ". authority: " + authority); 675 676 Trace.beginSection(traceSectionName("executeSyncReset", isLocal)); 677 try (PickerDbFacade.DbWriteOperation operation = 678 mDbFacade.beginResetMediaOperation(authority)) { 679 final int writeCount = operation.execute(null /* cursor */); 680 operation.setSuccess(); 681 682 Log.i(TAG, "SyncReset. isLocal:" + isLocal + ". authority: " + authority 683 + ". result count: " + writeCount); 684 } finally { 685 Trace.endSection(); 686 } 687 } 688 executeSyncAlbumReset(String authority, boolean isLocal, String albumId)689 private void executeSyncAlbumReset(String authority, boolean isLocal, String albumId) { 690 Log.i(TAG, "Executing SyncAlbumReset." 691 + " isLocal: " + isLocal + ". authority: " + authority + ". albumId: " + albumId); 692 693 Trace.beginSection(traceSectionName("executeSyncAlbumReset", isLocal)); 694 try (PickerDbFacade.DbWriteOperation operation = 695 mDbFacade.beginResetAlbumMediaOperation(authority, albumId)) { 696 final int writeCount = operation.execute(null /* cursor */); 697 operation.setSuccess(); 698 699 Log.i(TAG, "Successfully executed SyncResetAlbum. authority: " + authority 700 + ". albumId: " + albumId + ". Result count: " + writeCount); 701 } finally { 702 Trace.endSection(); 703 } 704 } 705 executeSyncAdd(String authority, boolean isLocal, String expectedMediaCollectionId, boolean isIncrementalSync, Bundle queryArgs)706 private void executeSyncAdd(String authority, boolean isLocal, 707 String expectedMediaCollectionId, boolean isIncrementalSync, Bundle queryArgs) { 708 final Uri uri = getMediaUri(authority); 709 final List<String> expectedHonoredArgs = new ArrayList<>(); 710 if (isIncrementalSync) { 711 expectedHonoredArgs.add(EXTRA_SYNC_GENERATION); 712 } 713 714 Log.i(TAG, "Executing SyncAdd. isLocal: " + isLocal + ". authority: " + authority); 715 716 Trace.beginSection(traceSectionName("executeSyncAdd", isLocal)); 717 try (PickerDbFacade.DbWriteOperation operation = 718 mDbFacade.beginAddMediaOperation(authority)) { 719 executePagedSync(uri, expectedMediaCollectionId, expectedHonoredArgs, queryArgs, 720 operation); 721 } finally { 722 Trace.endSection(); 723 } 724 } 725 executeSyncAddAlbum(String authority, boolean isLocal, String albumId, Bundle queryArgs)726 private void executeSyncAddAlbum(String authority, boolean isLocal, 727 String albumId, Bundle queryArgs) { 728 final Uri uri = getMediaUri(authority); 729 730 Log.i(TAG, "Executing SyncAddAlbum. " 731 + "isLocal: " + isLocal + ". authority: " + authority + ". albumId: " + albumId); 732 733 Trace.beginSection(traceSectionName("executeSyncAddAlbum", isLocal)); 734 try (PickerDbFacade.DbWriteOperation operation = 735 mDbFacade.beginAddAlbumMediaOperation(authority, albumId)) { 736 737 // We don't need to validate the mediaCollectionId for album_media sync since it's 738 // always a full sync 739 executePagedSync(uri, /* mediaCollectionId */ null, Arrays.asList(EXTRA_ALBUM_ID), 740 queryArgs, operation); 741 } finally { 742 Trace.endSection(); 743 } 744 } 745 executeSyncRemove(String authority, boolean isLocal, String mediaCollectionId, Bundle queryArgs)746 private void executeSyncRemove(String authority, boolean isLocal, 747 String mediaCollectionId, Bundle queryArgs) { 748 final Uri uri = getDeletedMediaUri(authority); 749 750 Log.i(TAG, "Executing SyncRemove. isLocal: " + isLocal + ". authority: " + authority); 751 752 Trace.beginSection(traceSectionName("executeSyncRemove", isLocal)); 753 try (PickerDbFacade.DbWriteOperation operation = 754 mDbFacade.beginRemoveMediaOperation(authority)) { 755 executePagedSync(uri, mediaCollectionId, Arrays.asList(EXTRA_SYNC_GENERATION), 756 queryArgs, operation); 757 } finally { 758 Trace.endSection(); 759 } 760 } 761 762 /** 763 * Persist cloud provider info and send a sync request to the background thread. 764 */ persistCloudProviderInfo(@onNull CloudProviderInfo info, boolean shouldUnset)765 private void persistCloudProviderInfo(@NonNull CloudProviderInfo info, boolean shouldUnset) { 766 synchronized (mCloudProviderLock) { 767 setCurrentCloudProviderInfo(info); 768 769 final String authority = info.authority; 770 final SharedPreferences.Editor editor = mUserPrefs.edit(); 771 final boolean isCloudProviderInfoNotEmpty = !info.isEmpty(); 772 773 if (isCloudProviderInfoNotEmpty) { 774 editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, authority); 775 } else if (shouldUnset) { 776 editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, 777 PREFS_VALUE_CLOUD_PROVIDER_UNSET); 778 } else { 779 editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY); 780 } 781 782 editor.putBoolean( 783 PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, isCloudProviderInfoNotEmpty); 784 785 editor.apply(); 786 787 if (SdkLevel.isAtLeastT()) { 788 try { 789 StorageManager sm = mContext.getSystemService(StorageManager.class); 790 sm.setCloudMediaProvider(authority); 791 } catch (SecurityException e) { 792 // When run as part of the unit tests, the notification fails because only the 793 // MediaProvider uid can notify 794 Log.w(TAG, "Failed to notify the system of cloud provider update to: " 795 + authority); 796 } 797 } 798 799 Log.d(TAG, "Updated cloud provider to: " + authority); 800 801 resetCachedMediaCollectionInfo(info.authority, /* isLocal */ false); 802 } 803 } 804 805 /** 806 * Clears the persisted cloud provider authority and sets the state to default (NOT_SET). 807 */ 808 @VisibleForTesting clearPersistedCloudProviderAuthority()809 void clearPersistedCloudProviderAuthority() { 810 Log.d(TAG, "Setting the cloud provider state to default (NOT_SET) by clearing the " 811 + "persisted cloud provider authority"); 812 mUserPrefs.edit().remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY).apply(); 813 } 814 815 /** 816 * Commit the latest media collection info when a sync operation is completed. 817 */ cacheMediaCollectionInfo(@ullable String authority, boolean isLocal, @Nullable Bundle bundle)818 private boolean cacheMediaCollectionInfo(@Nullable String authority, boolean isLocal, 819 @Nullable Bundle bundle) { 820 if (authority == null) { 821 Log.d(TAG, "Ignoring cache media info for null authority with bundle: " + bundle); 822 return true; 823 } 824 825 Trace.beginSection(traceSectionName("cacheMediaCollectionInfo", isLocal)); 826 827 try { 828 if (isLocal) { 829 cacheMediaCollectionInfoInternal(isLocal, bundle); 830 return true; 831 } else { 832 synchronized (mCloudProviderLock) { 833 // Check if the media collection info belongs to the current cloud provider 834 // authority. 835 if (Objects.equals(authority, mCloudProviderInfo.authority)) { 836 cacheMediaCollectionInfoInternal(isLocal, bundle); 837 return true; 838 } else { 839 Log.e(TAG, "Do not cache collection info for " 840 + authority + " because cloud provider changed to " 841 + mCloudProviderInfo.authority); 842 return false; 843 } 844 } 845 } 846 } finally { 847 Trace.endSection(); 848 } 849 } 850 cacheMediaCollectionInfoInternal(boolean isLocal, @Nullable Bundle bundle)851 private void cacheMediaCollectionInfoInternal(boolean isLocal, 852 @Nullable Bundle bundle) { 853 final SharedPreferences.Editor editor = mSyncPrefs.edit(); 854 if (bundle == null) { 855 editor.remove(getPrefsKey(isLocal, MEDIA_COLLECTION_ID)); 856 editor.remove(getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION)); 857 } else { 858 final String collectionId = bundle.getString(MEDIA_COLLECTION_ID); 859 final long generation = bundle.getLong(LAST_MEDIA_SYNC_GENERATION); 860 861 editor.putString(getPrefsKey(isLocal, MEDIA_COLLECTION_ID), collectionId); 862 editor.putLong(getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), generation); 863 } 864 editor.apply(); 865 } 866 resetCachedMediaCollectionInfo(@ullable String authority, boolean isLocal)867 private boolean resetCachedMediaCollectionInfo(@Nullable String authority, boolean isLocal) { 868 return cacheMediaCollectionInfo(authority, isLocal, /* bundle */ null); 869 } 870 getCachedMediaCollectionInfo(boolean isLocal)871 private Bundle getCachedMediaCollectionInfo(boolean isLocal) { 872 final Bundle bundle = new Bundle(); 873 874 final String collectionId = mSyncPrefs.getString( 875 getPrefsKey(isLocal, MEDIA_COLLECTION_ID), /* default */ null); 876 final long generation = mSyncPrefs.getLong( 877 getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), /* default */ -1); 878 879 bundle.putString(MEDIA_COLLECTION_ID, collectionId); 880 bundle.putLong(LAST_MEDIA_SYNC_GENERATION, generation); 881 882 return bundle; 883 } 884 getLatestMediaCollectionInfo(String authority)885 private Bundle getLatestMediaCollectionInfo(String authority) { 886 return mContext.getContentResolver().call(getMediaCollectionInfoUri(authority), 887 CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, 888 /* extras */ null); 889 } 890 891 @NonNull getSyncRequestParams(@ullable String authority, boolean isLocal)892 private SyncRequestParams getSyncRequestParams(@Nullable String authority, 893 boolean isLocal) throws RequestObsoleteException { 894 if (isLocal) { 895 return getSyncRequestParamsInternal(authority, isLocal); 896 } else { 897 // Ensure that we are fetching sync request params for the current cloud provider. 898 synchronized (mCloudProviderLock) { 899 if (Objects.equals(mCloudProviderInfo.authority, authority)) { 900 return getSyncRequestParamsInternal(authority, isLocal); 901 } else { 902 throw new RequestObsoleteException("Attempt to fetch sync request params for an" 903 + " unknown cloud provider. Current provider: " 904 + mCloudProviderInfo.authority + " Requested provider: " + authority); 905 } 906 } 907 } 908 } 909 910 911 @NonNull getSyncRequestParamsInternal(@ullable String authority, boolean isLocal)912 private SyncRequestParams getSyncRequestParamsInternal(@Nullable String authority, 913 boolean isLocal) { 914 Log.d(TAG, "getSyncRequestParams() " + (isLocal ? "LOCAL" : "CLOUD") 915 + ", auth=" + authority); 916 if (DEBUG) { 917 Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable()); 918 } 919 920 final SyncRequestParams result; 921 if (authority == null) { 922 // Only cloud authority can be null 923 result = SyncRequestParams.forResetMedia(); 924 } else { 925 final Bundle cachedMediaCollectionInfo = getCachedMediaCollectionInfo(isLocal); 926 final Bundle latestMediaCollectionInfo = getLatestMediaCollectionInfo(authority); 927 928 final String latestCollectionId = 929 latestMediaCollectionInfo.getString(MEDIA_COLLECTION_ID); 930 final long latestGeneration = 931 latestMediaCollectionInfo.getLong(LAST_MEDIA_SYNC_GENERATION); 932 Log.d(TAG, " Latest ID/Gen=" + latestCollectionId + "/" + latestGeneration); 933 934 final String cachedCollectionId = 935 cachedMediaCollectionInfo.getString(MEDIA_COLLECTION_ID); 936 final long cachedGeneration = 937 cachedMediaCollectionInfo.getLong(LAST_MEDIA_SYNC_GENERATION); 938 Log.d(TAG, " Cached ID/Gen=" + cachedCollectionId + "/" + cachedGeneration); 939 940 if (TextUtils.isEmpty(latestCollectionId) || latestGeneration < 0) { 941 throw new IllegalStateException("Unexpected Latest Media Collection Info: " 942 + "ID/Gen=" + latestCollectionId + "/" + latestGeneration); 943 } 944 945 if (!Objects.equals(latestCollectionId, cachedCollectionId)) { 946 result = SyncRequestParams.forFullMedia(latestMediaCollectionInfo); 947 } else if (cachedGeneration == latestGeneration) { 948 result = SyncRequestParams.forNone(); 949 } else { 950 result = SyncRequestParams.forIncremental( 951 cachedGeneration, latestMediaCollectionInfo); 952 } 953 } 954 Log.d(TAG, " RESULT=" + result); 955 return result; 956 } 957 getPrefsKey(boolean isLocal, String key)958 private String getPrefsKey(boolean isLocal, String key) { 959 return (isLocal ? PREFS_KEY_LOCAL_PREFIX : PREFS_KEY_CLOUD_PREFIX) + key; 960 } 961 query(Uri uri, Bundle extras)962 private Cursor query(Uri uri, Bundle extras) { 963 return mContext.getContentResolver().query(uri, /* projection */ null, extras, 964 /* cancellationSignal */ null); 965 } 966 executePagedSync(Uri uri, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Bundle queryArgs, PickerDbFacade.DbWriteOperation dbWriteOperation)967 private void executePagedSync(Uri uri, String expectedMediaCollectionId, 968 List<String> expectedHonoredArgs, Bundle queryArgs, 969 PickerDbFacade.DbWriteOperation dbWriteOperation) { 970 Trace.beginSection(traceSectionName("executePagedSync")); 971 try { 972 int cursorCount = 0; 973 int totalRowcount = 0; 974 // Set to check the uniqueness of tokens across pages. 975 Set<String> tokens = new ArraySet<>(); 976 977 String nextPageToken = null; 978 do { 979 if (nextPageToken != null) { 980 queryArgs.putString(EXTRA_PAGE_TOKEN, nextPageToken); 981 } 982 983 try (Cursor cursor = query(uri, queryArgs)) { 984 nextPageToken = validateCursor(cursor, expectedMediaCollectionId, 985 expectedHonoredArgs, tokens); 986 987 int writeCount = dbWriteOperation.execute(cursor); 988 989 totalRowcount += writeCount; 990 cursorCount += cursor.getCount(); 991 } 992 } while (nextPageToken != null); 993 994 dbWriteOperation.setSuccess(); 995 Log.i(TAG, "Paged sync successful. QueryArgs: " + queryArgs + ". Result count: " 996 + totalRowcount + ". Cursor count: " + cursorCount); 997 } finally { 998 Trace.endSection(); 999 } 1000 } 1001 1002 /** 1003 * Get the default {@link CloudProviderInfo} at {@link PickerSyncController} construction 1004 */ 1005 @VisibleForTesting getDefaultCloudProviderInfo(@ullable String lastProvider)1006 CloudProviderInfo getDefaultCloudProviderInfo(@Nullable String lastProvider) { 1007 final List<CloudProviderInfo> providers = getAvailableCloudProviders(); 1008 1009 if (providers.size() == 1) { 1010 Log.i(TAG, "Only 1 cloud provider found, hence " + providers.get(0).authority 1011 + " is the default"); 1012 return providers.get(0); 1013 } else { 1014 Log.i(TAG, "Found " + providers.size() + " available Cloud Media Providers."); 1015 } 1016 1017 if (lastProvider != null) { 1018 for (CloudProviderInfo provider : providers) { 1019 if (Objects.equals(provider.authority, lastProvider)) { 1020 return provider; 1021 } 1022 } 1023 } 1024 1025 final String defaultProviderPkg = mConfigStore.getDefaultCloudProviderPackage(); 1026 if (defaultProviderPkg != null) { 1027 Log.i(TAG, "Default Cloud-Media-Provider package is " + defaultProviderPkg); 1028 1029 for (CloudProviderInfo provider : providers) { 1030 if (provider.matches(defaultProviderPkg)) { 1031 return provider; 1032 } 1033 } 1034 } else { 1035 Log.i(TAG, "Default Cloud-Media-Provider is not set."); 1036 } 1037 1038 // No default set or default not installed 1039 return CloudProviderInfo.EMPTY; 1040 } 1041 traceSectionName(@onNull String method)1042 private static String traceSectionName(@NonNull String method) { 1043 return "PSC." + method; 1044 } 1045 traceSectionName(@onNull String method, boolean isLocal)1046 private static String traceSectionName(@NonNull String method, boolean isLocal) { 1047 return traceSectionName(method) 1048 + "[" + (isLocal ? "local" : "cloud") + ']'; 1049 } 1050 validateCursor(Cursor cursor, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Set<String> usedPageTokens)1051 private static String validateCursor(Cursor cursor, String expectedMediaCollectionId, 1052 List<String> expectedHonoredArgs, Set<String> usedPageTokens) { 1053 final Bundle bundle = cursor.getExtras(); 1054 1055 if (bundle == null) { 1056 throw new IllegalStateException("Unable to verify the media collection id"); 1057 } 1058 1059 final String mediaCollectionId = bundle.getString(EXTRA_MEDIA_COLLECTION_ID); 1060 final String pageToken = bundle.getString(EXTRA_PAGE_TOKEN); 1061 List<String> honoredArgs = bundle.getStringArrayList(EXTRA_HONORED_ARGS); 1062 if (honoredArgs == null) { 1063 honoredArgs = new ArrayList<>(); 1064 } 1065 1066 if (expectedMediaCollectionId != null 1067 && !expectedMediaCollectionId.equals(mediaCollectionId)) { 1068 throw new IllegalStateException("Mismatched media collection id. Expected: " 1069 + expectedMediaCollectionId + ". Found: " + mediaCollectionId); 1070 } 1071 1072 if (!honoredArgs.containsAll(expectedHonoredArgs)) { 1073 throw new IllegalStateException("Unspecified honored args. Expected: " 1074 + Arrays.toString(expectedHonoredArgs.toArray()) 1075 + ". Found: " + Arrays.toString(honoredArgs.toArray())); 1076 } 1077 1078 if (usedPageTokens.contains(pageToken)) { 1079 throw new IllegalStateException("Found repeated page token: " + pageToken); 1080 } else { 1081 usedPageTokens.add(pageToken); 1082 } 1083 1084 return pageToken; 1085 } 1086 1087 private static class SyncRequestParams { 1088 static final SyncRequestParams SYNC_REQUEST_NONE = new SyncRequestParams(SYNC_TYPE_NONE); 1089 static final SyncRequestParams SYNC_REQUEST_MEDIA_RESET = 1090 new SyncRequestParams(SYNC_TYPE_MEDIA_RESET); 1091 1092 final int syncType; 1093 // Only valid for SYNC_TYPE_INCREMENTAL 1094 final long syncGeneration; 1095 // Only valid for SYNC_TYPE_[INCREMENTAL|FULL] 1096 final Bundle latestMediaCollectionInfo; 1097 SyncRequestParams(@yncType int syncType)1098 SyncRequestParams(@SyncType int syncType) { 1099 this(syncType, /* syncGeneration */ 0, /* latestMediaCollectionInfo */ null); 1100 } 1101 SyncRequestParams(@yncType int syncType, long syncGeneration, Bundle latestMediaCollectionInfo)1102 SyncRequestParams(@SyncType int syncType, long syncGeneration, 1103 Bundle latestMediaCollectionInfo) { 1104 this.syncType = syncType; 1105 this.syncGeneration = syncGeneration; 1106 this.latestMediaCollectionInfo = latestMediaCollectionInfo; 1107 } 1108 getMediaCollectionId()1109 String getMediaCollectionId() { 1110 return latestMediaCollectionInfo.getString(MEDIA_COLLECTION_ID); 1111 } 1112 forNone()1113 static SyncRequestParams forNone() { 1114 return SYNC_REQUEST_NONE; 1115 } 1116 forResetMedia()1117 static SyncRequestParams forResetMedia() { 1118 return SYNC_REQUEST_MEDIA_RESET; 1119 } 1120 forFullMedia(Bundle latestMediaCollectionInfo)1121 static SyncRequestParams forFullMedia(Bundle latestMediaCollectionInfo) { 1122 return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL, /* generation */ 0, 1123 latestMediaCollectionInfo); 1124 } 1125 forIncremental(long generation, Bundle latestMediaCollectionInfo)1126 static SyncRequestParams forIncremental(long generation, Bundle latestMediaCollectionInfo) { 1127 return new SyncRequestParams(SYNC_TYPE_MEDIA_INCREMENTAL, generation, 1128 latestMediaCollectionInfo); 1129 } 1130 1131 @Override toString()1132 public String toString() { 1133 return "SyncRequestParams{type=" + syncTypeToString(syncType) 1134 + ", gen=" + syncGeneration + ", latest=" + latestMediaCollectionInfo + '}'; 1135 } 1136 } 1137 syncTypeToString(@yncType int syncType)1138 private static String syncTypeToString(@SyncType int syncType) { 1139 switch (syncType) { 1140 case SYNC_TYPE_NONE: 1141 return "NONE"; 1142 case SYNC_TYPE_MEDIA_INCREMENTAL: 1143 return "MEDIA_INCREMENTAL"; 1144 case SYNC_TYPE_MEDIA_FULL: 1145 return "MEDIA_FULL"; 1146 case SYNC_TYPE_MEDIA_RESET: 1147 return "MEDIA_RESET"; 1148 default: 1149 return "Unknown"; 1150 } 1151 } 1152 isCloudProviderUnset(@ullable String lastProviderAuthority)1153 private static boolean isCloudProviderUnset(@Nullable String lastProviderAuthority) { 1154 return Objects.equals(lastProviderAuthority, PREFS_VALUE_CLOUD_PROVIDER_UNSET); 1155 } 1156 } 1157