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