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; 25 26 import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri; 27 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri; 28 import static com.android.providers.media.PickerUriResolver.getMediaUri; 29 30 import android.annotation.IntDef; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.SharedPreferences; 34 import android.content.pm.ApplicationInfo; 35 import android.content.pm.PackageManager; 36 import android.content.pm.PackageManager.NameNotFoundException; 37 import android.content.pm.ProviderInfo; 38 import android.content.pm.ResolveInfo; 39 import android.database.Cursor; 40 import android.net.Uri; 41 import android.os.Bundle; 42 import android.os.Process; 43 import android.os.storage.StorageManager; 44 import android.provider.CloudMediaProvider; 45 import android.provider.CloudMediaProviderContract; 46 import android.text.TextUtils; 47 import android.util.ArraySet; 48 import android.util.Log; 49 import android.widget.Toast; 50 51 import androidx.annotation.GuardedBy; 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.R; 57 import com.android.providers.media.photopicker.data.PickerDbFacade; 58 import com.android.providers.media.util.ForegroundThread; 59 import com.android.providers.media.util.StringUtils; 60 61 import java.lang.annotation.Retention; 62 import java.lang.annotation.RetentionPolicy; 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.Set; 68 69 /** 70 * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances on the device 71 * into the picker db. 72 */ 73 public class PickerSyncController { 74 private static final String TAG = "PickerSyncController"; 75 76 public static final String SYNC_DELAY_MS = "default_sync_delay_ms"; 77 public static final String ALLOWED_CLOUD_PROVIDERS_KEY = "allowed_cloud_providers"; 78 79 private static final String PREFS_KEY_CLOUD_PROVIDER_AUTHORITY = "cloud_provider_authority"; 80 private static final String PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION = 81 "cloud_provider_pending_notification"; 82 private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:"; 83 private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:"; 84 85 private static final String PICKER_USER_PREFS_FILE_NAME = "picker_user_prefs"; 86 public static final String PICKER_SYNC_PREFS_FILE_NAME = "picker_sync_prefs"; 87 public static final String LOCAL_PICKER_PROVIDER_AUTHORITY = 88 "com.android.providers.media.photopicker"; 89 90 private static final int SYNC_TYPE_NONE = 0; 91 private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1; 92 private static final int SYNC_TYPE_MEDIA_FULL = 2; 93 private static final int SYNC_TYPE_MEDIA_RESET = 3; 94 95 @IntDef(flag = false, prefix = { "SYNC_TYPE_" }, value = { 96 SYNC_TYPE_NONE, 97 SYNC_TYPE_MEDIA_INCREMENTAL, 98 SYNC_TYPE_MEDIA_FULL, 99 SYNC_TYPE_MEDIA_RESET, 100 }) 101 @Retention(RetentionPolicy.SOURCE) 102 private @interface SyncType {} 103 104 private final Object mLock = new Object(); 105 private final PickerDbFacade mDbFacade; 106 private final Context mContext; 107 private final SharedPreferences mSyncPrefs; 108 private final SharedPreferences mUserPrefs; 109 private final String mLocalProvider; 110 private final long mSyncDelayMs; 111 private final Runnable mSyncAllMediaCallback; 112 private final Set<String> mAllowedCloudProviders; 113 114 @GuardedBy("mLock") 115 private CloudProviderInfo mCloudProviderInfo; 116 PickerSyncController(Context context, PickerDbFacade dbFacade, String localProvider, String allowedCloudProviders, long syncDelayMs)117 public PickerSyncController(Context context, PickerDbFacade dbFacade, 118 String localProvider, String allowedCloudProviders, long syncDelayMs) { 119 mContext = context; 120 mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME, 121 Context.MODE_PRIVATE); 122 mUserPrefs = mContext.getSharedPreferences(PICKER_USER_PREFS_FILE_NAME, 123 Context.MODE_PRIVATE); 124 mDbFacade = dbFacade; 125 mLocalProvider = localProvider; 126 mSyncDelayMs = syncDelayMs; 127 mSyncAllMediaCallback = this::syncAllMedia; 128 129 final String cachedAuthority = mUserPrefs.getString( 130 PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, null); 131 132 mAllowedCloudProviders = parseAllowedCloudProviders(allowedCloudProviders); 133 134 final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority); 135 136 if (Objects.equals(defaultInfo.authority, cachedAuthority)) { 137 // Just set it without persisting since it's not changing and persisting would 138 // notify the user that cloud media is now available 139 mCloudProviderInfo = defaultInfo; 140 } else { 141 // Persist it so that we notify the user that cloud media is now available 142 persistCloudProviderInfo(defaultInfo); 143 } 144 145 Log.d(TAG, "Initialized cloud provider to: " + mCloudProviderInfo.authority); 146 } 147 148 /** 149 * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances 150 */ syncAllMedia()151 public void syncAllMedia() { 152 syncAllMediaFromProvider(mLocalProvider, /* retryOnFailure */ true); 153 154 synchronized (mLock) { 155 final String cloudProvider = mCloudProviderInfo.authority; 156 157 syncAllMediaFromProvider(cloudProvider, /* retryOnFailure */ true); 158 159 // Reset the album_media table every time we sync all media 160 resetAlbumMedia(); 161 162 // Set the latest cloud provider on the facade 163 mDbFacade.setCloudProvider(cloudProvider); 164 } 165 } 166 167 /** 168 * Syncs album media from the local and currently enabled cloud {@link CloudMediaProvider} 169 * instances 170 */ syncAlbumMedia(String albumId, boolean isLocal)171 public void syncAlbumMedia(String albumId, boolean isLocal) { 172 if (isLocal) { 173 syncAlbumMediaFromProvider(mLocalProvider, albumId); 174 } else { 175 synchronized (mLock) { 176 syncAlbumMediaFromProvider(mCloudProviderInfo.authority, albumId); 177 } 178 } 179 } 180 resetAlbumMedia()181 private void resetAlbumMedia() { 182 executeSyncAlbumReset(mLocalProvider, /* albumId */ null); 183 184 synchronized (mLock) { 185 final String cloudProvider = mCloudProviderInfo.authority; 186 executeSyncAlbumReset(cloudProvider, /* albumId */ null); 187 } 188 } 189 resetAllMedia(String authority)190 private void resetAllMedia(String authority) { 191 executeSyncReset(authority); 192 resetCachedMediaCollectionInfo(authority); 193 } 194 195 /** 196 * Returns the supported cloud {@link CloudMediaProvider} infos. 197 */ getCloudProviderInfo(String authority)198 public CloudProviderInfo getCloudProviderInfo(String authority) { 199 for (CloudProviderInfo info : getSupportedCloudProviders(/* ignoreAllowList */ false)) { 200 if (info.authority.equals(authority)) { 201 return info; 202 } 203 } 204 205 return CloudProviderInfo.EMPTY; 206 } 207 208 /** 209 * Returns the supported cloud {@link CloudMediaProvider} authorities. 210 */ 211 @VisibleForTesting getSupportedCloudProviders()212 List<CloudProviderInfo> getSupportedCloudProviders() { 213 return getSupportedCloudProviders(/* ignoreAllowList */ false); 214 } 215 getSupportedCloudProviders(boolean ignoreAllowList)216 private List<CloudProviderInfo> getSupportedCloudProviders(boolean ignoreAllowList) { 217 final List<CloudProviderInfo> result = new ArrayList<>(); 218 219 final PackageManager pm = mContext.getPackageManager(); 220 final Intent intent = new Intent(CloudMediaProviderContract.PROVIDER_INTERFACE); 221 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, /* flags */ 0); 222 223 for (ResolveInfo info : providers) { 224 ProviderInfo providerInfo = info.providerInfo; 225 if (providerInfo.authority != null 226 && CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals( 227 providerInfo.readPermission) 228 && (ignoreAllowList 229 || mAllowedCloudProviders.contains(providerInfo.authority))) { 230 result.add(new CloudProviderInfo(providerInfo.authority, 231 providerInfo.applicationInfo.packageName, 232 providerInfo.applicationInfo.uid)); 233 } 234 } 235 236 return result; 237 } 238 239 /** 240 * Enables a provider with {@code authority} as the default cloud {@link CloudMediaProvider}. 241 * If {@code authority} is set to {@code null}, it simply clears the cloud provider. 242 * 243 * Note, that this doesn't sync the new provider after switching, however, no cloud items will 244 * be available from the picker db until the next sync. Callers should schedule a sync in the 245 * background after switching providers. 246 * 247 * @return {@code true} if the provider was successfully enabled or cleared, {@code false} 248 * otherwise 249 */ setCloudProvider(String authority)250 public boolean setCloudProvider(String authority) { 251 synchronized (mLock) { 252 if (Objects.equals(mCloudProviderInfo.authority, authority)) { 253 Log.w(TAG, "Cloud provider already set: " + authority); 254 return true; 255 } 256 } 257 258 final CloudProviderInfo newProviderInfo = getCloudProviderInfo(authority); 259 if (authority == null || !newProviderInfo.isEmpty()) { 260 synchronized (mLock) { 261 final String oldAuthority = mCloudProviderInfo.authority; 262 persistCloudProviderInfo(newProviderInfo); 263 resetCachedMediaCollectionInfo(newProviderInfo.authority); 264 265 // Disable cloud provider queries on the db until next sync 266 // This will temporarily *clear* the cloud provider on the db facade and prevent 267 // any queries from seeing cloud media until a sync where the cloud provider will be 268 // reset on the facade 269 mDbFacade.setCloudProvider(null); 270 271 Log.i(TAG, "Cloud provider changed successfully. Old: " 272 + oldAuthority + ". New: " + newProviderInfo.authority); 273 } 274 275 return true; 276 } 277 278 Log.w(TAG, "Cloud provider not supported: " + authority); 279 return false; 280 } 281 282 /** 283 * Set cloud provider and update allowed cloud providers 284 */ 285 @VisibleForTesting forceSetCloudProvider(String authority)286 public void forceSetCloudProvider(String authority) { 287 if (authority == null) { 288 mAllowedCloudProviders.clear(); 289 } else { 290 mAllowedCloudProviders.add(authority); 291 } 292 293 setCloudProvider(authority); 294 } 295 getCloudProvider()296 public String getCloudProvider() { 297 synchronized (mLock) { 298 return mCloudProviderInfo.authority; 299 } 300 } 301 getLocalProvider()302 public String getLocalProvider() { 303 return mLocalProvider; 304 } 305 isProviderEnabled(String authority)306 public boolean isProviderEnabled(String authority) { 307 if (mLocalProvider.equals(authority)) { 308 return true; 309 } 310 311 synchronized (mLock) { 312 if (!mCloudProviderInfo.isEmpty() && mCloudProviderInfo.authority.equals(authority)) { 313 return true; 314 } 315 } 316 317 return false; 318 } 319 isProviderEnabled(String authority, int uid)320 public boolean isProviderEnabled(String authority, int uid) { 321 if (uid == Process.myUid() && mLocalProvider.equals(authority)) { 322 return true; 323 } 324 325 synchronized (mLock) { 326 if (!mCloudProviderInfo.isEmpty() && uid == mCloudProviderInfo.uid 327 && mCloudProviderInfo.authority.equals(authority)) { 328 return true; 329 } 330 } 331 332 return false; 333 } 334 isProviderSupported(String authority, int uid)335 public boolean isProviderSupported(String authority, int uid) { 336 if (uid == Process.myUid() && mLocalProvider.equals(authority)) { 337 return true; 338 } 339 340 // TODO(b/232738117): Enforce allow list here. This works around some CTS failure late in 341 // Android T. The current implementation is fine since cloud providers is only supported 342 // for app developers testing. 343 final List<CloudProviderInfo> infos = getSupportedCloudProviders( 344 /* ignoreAllowList */ true); 345 for (CloudProviderInfo info : infos) { 346 if (info.uid == uid && info.authority.equals(authority)) { 347 return true; 348 } 349 } 350 351 return false; 352 } 353 354 /** 355 * Notifies about media events like inserts/updates/deletes from cloud and local providers and 356 * syncs the changes in the background. 357 * 358 * There is a delay before executing the background sync to artificially throttle the burst 359 * notifications. 360 */ notifyMediaEvent()361 public void notifyMediaEvent() { 362 BackgroundThread.getHandler().removeCallbacks(mSyncAllMediaCallback); 363 BackgroundThread.getHandler().postDelayed(mSyncAllMediaCallback, mSyncDelayMs); 364 } 365 366 /** 367 * Notifies about package removal 368 */ notifyPackageRemoval(String packageName)369 public void notifyPackageRemoval(String packageName) { 370 synchronized (mLock) { 371 if (mCloudProviderInfo.matches(packageName)) { 372 Log.i(TAG, "Package " + packageName 373 + " is the current cloud provider and got removed"); 374 setCloudProvider(null); 375 } 376 } 377 } 378 379 /** 380 * Notifies about picker UI launched 381 */ notifyPickerLaunch()382 public void notifyPickerLaunch() { 383 final String packageName; 384 synchronized (mLock) { 385 packageName = mCloudProviderInfo.packageName; 386 } 387 388 final boolean hasPendingNotification = mUserPrefs.getBoolean( 389 PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false); 390 391 if (!hasPendingNotification || (packageName == null)) { 392 Log.d(TAG, "No pending UI notification"); 393 return; 394 } 395 396 // Offload showing the UI on a fg thread to avoid the expensive binder request 397 // to fetch the app name blocking the picker launch 398 ForegroundThread.getHandler().post(() -> { 399 Log.i(TAG, "Cloud media now available in the picker"); 400 401 final PackageManager pm = mContext.getPackageManager(); 402 String appName = packageName; 403 try { 404 ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); 405 appName = (String) pm.getApplicationLabel(appInfo); 406 } catch (final NameNotFoundException e) { 407 Log.i(TAG, "Failed to get appName for package: " + packageName); 408 } 409 410 final String message = mContext.getResources().getString(R.string.picker_cloud_sync, 411 appName); 412 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 413 }); 414 415 // Clear the notification 416 final SharedPreferences.Editor editor = mUserPrefs.edit(); 417 editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false); 418 editor.apply(); 419 } 420 syncAlbumMediaFromProvider(String authority, String albumId)421 private void syncAlbumMediaFromProvider(String authority, String albumId) { 422 final Bundle queryArgs = new Bundle(); 423 queryArgs.putString(EXTRA_ALBUM_ID, albumId); 424 425 try { 426 executeSyncAlbumReset(authority, albumId); 427 428 if (authority != null) { 429 executeSyncAddAlbum(authority, albumId, queryArgs); 430 } 431 } catch (RuntimeException e) { 432 // Unlike syncAllMediaFromProvider, we don't retry here because any errors would have 433 // occurred in fetching all the album_media since incremental sync is not supported. 434 // A full sync is therefore unlikely to resolve any issue 435 Log.e(TAG, "Failed to sync album media", e); 436 } 437 } 438 syncAllMediaFromProvider(String authority, boolean retryOnFailure)439 private void syncAllMediaFromProvider(String authority, boolean retryOnFailure) { 440 try { 441 final SyncRequestParams params = getSyncRequestParams(authority); 442 443 switch (params.syncType) { 444 case SYNC_TYPE_MEDIA_RESET: 445 // Can only happen when |authority| has been set to null and we need to clean up 446 resetAllMedia(authority); 447 break; 448 case SYNC_TYPE_MEDIA_FULL: 449 resetAllMedia(authority); 450 451 // Pass a mutable empty bundle intentionally because it might be populated with 452 // the next page token as part of a query to a cloud provider supporting 453 // pagination 454 executeSyncAdd(authority, params.getMediaCollectionId(), 455 /* isIncrementalSync */ false, /* queryArgs */ new Bundle()); 456 457 // Commit sync position 458 cacheMediaCollectionInfo(authority, params.latestMediaCollectionInfo); 459 break; 460 case SYNC_TYPE_MEDIA_INCREMENTAL: 461 final Bundle queryArgs = new Bundle(); 462 queryArgs.putLong(EXTRA_SYNC_GENERATION, params.syncGeneration); 463 464 executeSyncAdd(authority, params.getMediaCollectionId(), 465 /* isIncrementalSync */ true, queryArgs); 466 executeSyncRemove(authority, params.getMediaCollectionId(), queryArgs); 467 468 // Commit sync position 469 cacheMediaCollectionInfo(authority, params.latestMediaCollectionInfo); 470 break; 471 case SYNC_TYPE_NONE: 472 break; 473 default: 474 throw new IllegalArgumentException("Unexpected sync type: " + params.syncType); 475 } 476 } catch (RuntimeException e) { 477 // Reset all media for the cloud provider in case it never succeeds 478 resetAllMedia(authority); 479 480 // Attempt a full sync. If this fails, the db table would have been reset, 481 // flushing all old content and leaving the picker UI empty. 482 Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e); 483 if (retryOnFailure) { 484 syncAllMediaFromProvider(authority, /* retryOnFailure */ false); 485 } 486 } 487 } 488 executeSyncReset(String authority)489 private void executeSyncReset(String authority) { 490 Log.i(TAG, "Executing SyncReset. authority: " + authority); 491 492 try (PickerDbFacade.DbWriteOperation operation = 493 mDbFacade.beginResetMediaOperation(authority)) { 494 final int writeCount = operation.execute(null /* cursor */); 495 operation.setSuccess(); 496 497 Log.i(TAG, "SyncReset. Authority: " + authority + ". Result count: " + writeCount); 498 } 499 } 500 executeSyncAlbumReset(String authority, String albumId)501 private void executeSyncAlbumReset(String authority, String albumId) { 502 Log.i(TAG, "Executing SyncAlbumReset. authority: " + authority + ". albumId: " 503 + albumId); 504 505 try (PickerDbFacade.DbWriteOperation operation = 506 mDbFacade.beginResetAlbumMediaOperation(authority, albumId)) { 507 final int writeCount = operation.execute(null /* cursor */); 508 operation.setSuccess(); 509 510 Log.i(TAG, "Successfully executed SyncResetAlbum. authority: " + authority 511 + ". albumId: " + albumId + ". Result count: " + writeCount); 512 } 513 } 514 executeSyncAdd(String authority, String expectedMediaCollectionId, boolean isIncrementalSync, Bundle queryArgs)515 private void executeSyncAdd(String authority, String expectedMediaCollectionId, 516 boolean isIncrementalSync, Bundle queryArgs) { 517 final Uri uri = getMediaUri(authority); 518 final List<String> expectedHonoredArgs = new ArrayList<>(); 519 if (isIncrementalSync) { 520 expectedHonoredArgs.add(EXTRA_SYNC_GENERATION); 521 } 522 523 Log.i(TAG, "Executing SyncAdd. authority: " + authority); 524 try (PickerDbFacade.DbWriteOperation operation = 525 mDbFacade.beginAddMediaOperation(authority)) { 526 executePagedSync(uri, expectedMediaCollectionId, expectedHonoredArgs, queryArgs, 527 operation); 528 } 529 } 530 executeSyncAddAlbum(String authority, String albumId, Bundle queryArgs)531 private void executeSyncAddAlbum(String authority, String albumId, Bundle queryArgs) { 532 final Uri uri = getMediaUri(authority); 533 534 Log.i(TAG, "Executing SyncAddAlbum. authority: " + authority + ". albumId: " + albumId); 535 try (PickerDbFacade.DbWriteOperation operation = 536 mDbFacade.beginAddAlbumMediaOperation(authority, albumId)) { 537 538 // We don't need to validate the mediaCollectionId for album_media sync since it's 539 // always a full sync 540 executePagedSync(uri, /* mediaCollectionId */ null, Arrays.asList(EXTRA_ALBUM_ID), 541 queryArgs, operation); 542 } 543 } 544 executeSyncRemove(String authority, String mediaCollectionId, Bundle queryArgs)545 private void executeSyncRemove(String authority, String mediaCollectionId, Bundle queryArgs) { 546 final Uri uri = getDeletedMediaUri(authority); 547 548 Log.i(TAG, "Executing SyncRemove. authority: " + authority); 549 try (PickerDbFacade.DbWriteOperation operation = 550 mDbFacade.beginRemoveMediaOperation(authority)) { 551 executePagedSync(uri, mediaCollectionId, Arrays.asList(EXTRA_SYNC_GENERATION), 552 queryArgs, operation); 553 } 554 } 555 persistCloudProviderInfo(CloudProviderInfo info)556 private void persistCloudProviderInfo(CloudProviderInfo info) { 557 synchronized (mLock) { 558 mCloudProviderInfo = info; 559 } 560 561 final String authority = info.authority; 562 final SharedPreferences.Editor editor = mUserPrefs.edit(); 563 564 if (info.isEmpty()) { 565 editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY); 566 editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false); 567 } else { 568 editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, authority); 569 editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, true); 570 } 571 572 editor.apply(); 573 574 if (SdkLevel.isAtLeastT()) { 575 try { 576 StorageManager sm = mContext.getSystemService(StorageManager.class); 577 sm.setCloudMediaProvider(authority); 578 } catch (SecurityException e) { 579 // When run as part of the unit tests, the notification fails because only the 580 // MediaProvider uid can notify 581 Log.w(TAG, "Failed to notify the system of cloud provider update to: " + authority); 582 } 583 } 584 585 Log.d(TAG, "Updated cloud provider to: " + authority); 586 } 587 cacheMediaCollectionInfo(String authority, Bundle bundle)588 private void cacheMediaCollectionInfo(String authority, Bundle bundle) { 589 if (authority == null) { 590 Log.d(TAG, "Ignoring cache media info for null authority with bundle: " + bundle); 591 return; 592 } 593 594 final SharedPreferences.Editor editor = mSyncPrefs.edit(); 595 596 if (bundle == null) { 597 editor.remove(getPrefsKey(authority, MediaCollectionInfo.MEDIA_COLLECTION_ID)); 598 editor.remove(getPrefsKey(authority, MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION)); 599 } else { 600 final String collectionId = bundle.getString(MediaCollectionInfo.MEDIA_COLLECTION_ID); 601 final long generation = bundle.getLong( 602 MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION); 603 604 editor.putString(getPrefsKey(authority, MediaCollectionInfo.MEDIA_COLLECTION_ID), 605 collectionId); 606 editor.putLong(getPrefsKey(authority, MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION), 607 generation); 608 } 609 610 editor.apply(); 611 } 612 resetCachedMediaCollectionInfo(String authority)613 private void resetCachedMediaCollectionInfo(String authority) { 614 cacheMediaCollectionInfo(authority, /* bundle */ null); 615 } 616 getCachedMediaCollectionInfo(String authority)617 private Bundle getCachedMediaCollectionInfo(String authority) { 618 final Bundle bundle = new Bundle(); 619 620 final String collectionId = mSyncPrefs.getString( 621 getPrefsKey(authority, MediaCollectionInfo.MEDIA_COLLECTION_ID), 622 /* default */ null); 623 final long generation = mSyncPrefs.getLong( 624 getPrefsKey(authority, MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION), 625 /* default */ -1); 626 627 bundle.putString(MediaCollectionInfo.MEDIA_COLLECTION_ID, collectionId); 628 bundle.putLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, generation); 629 630 return bundle; 631 } 632 getLatestMediaCollectionInfo(String authority)633 private Bundle getLatestMediaCollectionInfo(String authority) { 634 return mContext.getContentResolver().call(getMediaCollectionInfoUri(authority), 635 CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, 636 /* extras */ null); 637 } 638 639 @SyncType getSyncRequestParams(String authority)640 private SyncRequestParams getSyncRequestParams(String authority) { 641 if (authority == null) { 642 // Only cloud authority can be null 643 Log.d(TAG, "Fetching SyncRequestParams. Null cloud authority. Result: SYNC_TYPE_RESET"); 644 return SyncRequestParams.forResetMedia(); 645 } 646 647 final Bundle cachedMediaCollectionInfo = getCachedMediaCollectionInfo(authority); 648 final Bundle latestMediaCollectionInfo = getLatestMediaCollectionInfo(authority); 649 650 final String latestCollectionId = 651 latestMediaCollectionInfo.getString(MediaCollectionInfo.MEDIA_COLLECTION_ID); 652 final long latestGeneration = 653 latestMediaCollectionInfo.getLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION); 654 655 final String cachedCollectionId = 656 cachedMediaCollectionInfo.getString(MediaCollectionInfo.MEDIA_COLLECTION_ID); 657 final long cachedGeneration = cachedMediaCollectionInfo.getLong( 658 MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION); 659 660 Log.d(TAG, "Fetching SyncRequestParams. Authority: " + authority 661 + ". LatestMediaCollectionInfo: " + latestMediaCollectionInfo 662 + ". CachedMediaCollectionInfo: " + cachedMediaCollectionInfo); 663 664 if (TextUtils.isEmpty(latestCollectionId) || latestGeneration < 0) { 665 throw new IllegalStateException("Unexpected media collection info. mediaCollectionId: " 666 + latestCollectionId + ". lastMediaSyncGeneration: " + latestGeneration); 667 } 668 669 if (!Objects.equals(latestCollectionId, cachedCollectionId)) { 670 Log.d(TAG, "SyncRequestParams. Authority: " + authority + ". Result: SYNC_TYPE_FULL"); 671 return SyncRequestParams.forFullMedia(latestMediaCollectionInfo); 672 } 673 674 if (cachedGeneration == latestGeneration) { 675 Log.d(TAG, "SyncRequestParams. Authority: " + authority + ". Result: SYNC_TYPE_NONE"); 676 return SyncRequestParams.forNone(); 677 } 678 679 Log.d(TAG, "SyncRequestParams. Authority: " + authority 680 + ". Result: SYNC_TYPE_INCREMENTAL"); 681 return SyncRequestParams.forIncremental(cachedGeneration, latestMediaCollectionInfo); 682 } 683 getPrefsKey(String authority, String key)684 private String getPrefsKey(String authority, String key) { 685 return (isLocal(authority) ? PREFS_KEY_LOCAL_PREFIX : PREFS_KEY_CLOUD_PREFIX) + key; 686 } 687 isLocal(String authority)688 private boolean isLocal(String authority) { 689 return mLocalProvider.equals(authority); 690 } 691 query(Uri uri, Bundle extras)692 private Cursor query(Uri uri, Bundle extras) { 693 return mContext.getContentResolver().query(uri, /* projection */ null, extras, 694 /* cancellationSignal */ null); 695 } 696 executePagedSync(Uri uri, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Bundle queryArgs, PickerDbFacade.DbWriteOperation dbWriteOperation)697 private void executePagedSync(Uri uri, String expectedMediaCollectionId, 698 List<String> expectedHonoredArgs, Bundle queryArgs, 699 PickerDbFacade.DbWriteOperation dbWriteOperation) { 700 int cursorCount = 0; 701 int totalRowcount = 0; 702 // Set to check the uniqueness of tokens across pages. 703 Set<String> tokens = new ArraySet<>(); 704 705 String nextPageToken = null; 706 do { 707 if (nextPageToken != null) { 708 queryArgs.putString(EXTRA_PAGE_TOKEN, nextPageToken); 709 } 710 711 try (Cursor cursor = query(uri, queryArgs)) { 712 nextPageToken = validateCursor(cursor, expectedMediaCollectionId, 713 expectedHonoredArgs, tokens); 714 715 int writeCount = dbWriteOperation.execute(cursor); 716 717 totalRowcount += writeCount; 718 cursorCount += cursor.getCount(); 719 } 720 } while (nextPageToken != null); 721 722 dbWriteOperation.setSuccess(); 723 Log.i(TAG, "Paged sync successful. QueryArgs: " + queryArgs + ". Result count: " 724 + totalRowcount + ". Cursor count: " + cursorCount); 725 } 726 getDefaultCloudProviderInfo(String cachedProvider)727 private CloudProviderInfo getDefaultCloudProviderInfo(String cachedProvider) { 728 final List<CloudProviderInfo> infos = 729 getSupportedCloudProviders(/* ignoreAllowList */ false); 730 731 if (infos.size() == 1) { 732 Log.i(TAG, "Only 1 cloud provider found, hence " 733 + infos.get(0).authority + " is the default"); 734 return infos.get(0); 735 } else { 736 final String defaultCloudProviderAuthority = StringUtils.getStringConfig( 737 mContext, R.string.config_default_cloud_provider_authority); 738 Log.i(TAG, "Found multiple cloud providers but OEM default is: " 739 + defaultCloudProviderAuthority); 740 741 if (cachedProvider != null) { 742 for (CloudProviderInfo info : infos) { 743 if (info.authority.equals(defaultCloudProviderAuthority)) { 744 return info; 745 } 746 } 747 } 748 749 if (defaultCloudProviderAuthority != null) { 750 for (CloudProviderInfo info : infos) { 751 if (info.authority.equals(defaultCloudProviderAuthority)) { 752 return info; 753 } 754 } 755 } 756 } 757 758 // No default set or default not installed 759 return CloudProviderInfo.EMPTY; 760 } 761 parseAllowedCloudProviders(String config)762 private Set<String> parseAllowedCloudProviders(String config) { 763 Set<String> allowedProviders = new ArraySet<>(); 764 final String[] allowedProvidersConfig = config.split(","); 765 766 if (allowedProvidersConfig.length == 0 || allowedProvidersConfig[0].isEmpty()) { 767 Log.i(TAG, "Empty allowed cloud providers"); 768 return allowedProviders; 769 } 770 771 for (String cloudProvider : allowedProvidersConfig) { 772 Log.d(TAG, "Parsed allowed cloud provider: " + cloudProvider + " from device config"); 773 allowedProviders.add(cloudProvider); 774 } 775 776 Log.i(TAG, "Parsed " + allowedProviders.size() + " allowed providers from device config"); 777 return allowedProviders; 778 } 779 validateCursor(Cursor cursor, String expectedMediaCollectionId, List<String> expectedHonoredArgs, Set<String> usedPageTokens)780 private static String validateCursor(Cursor cursor, String expectedMediaCollectionId, 781 List<String> expectedHonoredArgs, Set<String> usedPageTokens) { 782 final Bundle bundle = cursor.getExtras(); 783 784 if (bundle == null) { 785 throw new IllegalStateException("Unable to verify the media collection id"); 786 } 787 788 final String mediaCollectionId = bundle.getString(EXTRA_MEDIA_COLLECTION_ID); 789 final String pageToken = bundle.getString(EXTRA_PAGE_TOKEN); 790 List<String> honoredArgs = bundle.getStringArrayList(EXTRA_HONORED_ARGS); 791 if (honoredArgs == null) { 792 honoredArgs = new ArrayList<>(); 793 } 794 795 if (expectedMediaCollectionId != null 796 && !expectedMediaCollectionId.equals(mediaCollectionId)) { 797 throw new IllegalStateException("Mismatched media collection id. Expected: " 798 + expectedMediaCollectionId + ". Found: " + mediaCollectionId); 799 } 800 801 if (!honoredArgs.containsAll(expectedHonoredArgs)) { 802 throw new IllegalStateException("Unspecified honored args. Expected: " 803 + Arrays.toString(expectedHonoredArgs.toArray()) 804 + ". Found: " + Arrays.toString(honoredArgs.toArray())); 805 } 806 807 if (usedPageTokens.contains(pageToken)) { 808 throw new IllegalStateException("Found repeated page token: " + pageToken); 809 } else { 810 usedPageTokens.add(pageToken); 811 } 812 813 return pageToken; 814 } 815 816 @VisibleForTesting 817 static class CloudProviderInfo { 818 static final CloudProviderInfo EMPTY = new CloudProviderInfo(); 819 private final String authority; 820 private final String packageName; 821 private final int uid; 822 CloudProviderInfo()823 private CloudProviderInfo() { 824 this.authority = null; 825 this.packageName = null; 826 this.uid = -1; 827 } 828 CloudProviderInfo(String authority, String packageName, int uid)829 CloudProviderInfo(String authority, String packageName, int uid) { 830 Objects.requireNonNull(authority); 831 Objects.requireNonNull(packageName); 832 833 this.authority = authority; 834 this.packageName = packageName; 835 this.uid = uid; 836 } 837 isEmpty()838 boolean isEmpty() { 839 return equals(EMPTY); 840 } 841 matches(String packageName)842 boolean matches(String packageName) { 843 return !isEmpty() && this.packageName.equals(packageName); 844 } 845 846 @Override equals(Object obj)847 public boolean equals(Object obj) { 848 if (this == obj) return true; 849 if (obj == null || getClass() != obj.getClass()) return false; 850 851 CloudProviderInfo that = (CloudProviderInfo) obj; 852 853 return Objects.equals(authority, that.authority) && 854 Objects.equals(packageName, that.packageName) && 855 Objects.equals(uid, that.uid); 856 } 857 858 @Override hashCode()859 public int hashCode() { 860 return Objects.hash(authority, packageName, uid); 861 } 862 } 863 864 private static class SyncRequestParams { 865 private static final SyncRequestParams SYNC_REQUEST_NONE = 866 new SyncRequestParams(SYNC_TYPE_NONE); 867 private static final SyncRequestParams SYNC_REQUEST_MEDIA_RESET = 868 new SyncRequestParams(SYNC_TYPE_MEDIA_RESET); 869 870 private final int syncType; 871 // Only valid for SYNC_TYPE_INCREMENTAL 872 private final long syncGeneration; 873 // Only valid for SYNC_TYPE_[INCREMENTAL|FULL] 874 private final Bundle latestMediaCollectionInfo; 875 SyncRequestParams(@yncType int syncType)876 private SyncRequestParams(@SyncType int syncType) { 877 this(syncType, /* syncGeneration */ 0, /* latestMediaCollectionInfo */ null); 878 } 879 SyncRequestParams(@yncType int syncType, long syncGeneration, Bundle latestMediaCollectionInfo)880 private SyncRequestParams(@SyncType int syncType, long syncGeneration, 881 Bundle latestMediaCollectionInfo) { 882 this.syncType = syncType; 883 this.syncGeneration = syncGeneration; 884 this.latestMediaCollectionInfo = latestMediaCollectionInfo; 885 } 886 getMediaCollectionId()887 String getMediaCollectionId() { 888 return latestMediaCollectionInfo.getString(MediaCollectionInfo.MEDIA_COLLECTION_ID); 889 } 890 forNone()891 static SyncRequestParams forNone() { 892 return SYNC_REQUEST_NONE; 893 } 894 forResetMedia()895 static SyncRequestParams forResetMedia() { 896 return SYNC_REQUEST_MEDIA_RESET; 897 } 898 forFullMedia(Bundle latestMediaCollectionInfo)899 static SyncRequestParams forFullMedia(Bundle latestMediaCollectionInfo) { 900 return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL, /* generation */ 0, 901 latestMediaCollectionInfo); 902 } 903 forIncremental(long generation, Bundle latestMediaCollectionInfo)904 static SyncRequestParams forIncremental(long generation, Bundle latestMediaCollectionInfo) { 905 return new SyncRequestParams(SYNC_TYPE_MEDIA_INCREMENTAL, generation, 906 latestMediaCollectionInfo); 907 } 908 } 909 } 910