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