• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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