• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.sync;
18 
19 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markAlbumMediaSyncAsComplete;
20 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markAllMediaInMediaSetsSyncAsComplete;
21 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markAllSearchResultsSyncAsComplete;
22 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markMediaInMediaSetSyncAsComplete;
23 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markMediaSetsSyncAsComplete;
24 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSearchResultsSyncAsComplete;
25 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSyncAsComplete;
26 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewAlbumMediaSyncRequests;
27 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewMediaInMediaSetSyncRequest;
28 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewMediaSetsSyncRequest;
29 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewSearchResultsSyncRequests;
30 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewSyncRequests;
31 
32 import static java.util.Objects.requireNonNull;
33 
34 import android.content.Context;
35 import android.content.Intent;
36 import android.util.Log;
37 
38 import androidx.annotation.IntDef;
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.work.Constraints;
42 import androidx.work.Data;
43 import androidx.work.ExistingPeriodicWorkPolicy;
44 import androidx.work.ExistingWorkPolicy;
45 import androidx.work.OneTimeWorkRequest;
46 import androidx.work.Operation;
47 import androidx.work.OutOfQuotaPolicy;
48 import androidx.work.PeriodicWorkRequest;
49 import androidx.work.WorkInfo;
50 import androidx.work.WorkManager;
51 import androidx.work.Worker;
52 
53 import com.android.modules.utils.BackgroundThread;
54 import com.android.providers.media.ConfigStore;
55 import com.android.providers.media.flags.Flags;
56 import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
57 import com.android.providers.media.photopicker.v2.model.MediaInMediaSetSyncRequestParams;
58 import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
59 
60 import com.google.common.util.concurrent.ListenableFuture;
61 
62 import java.lang.annotation.Retention;
63 import java.lang.annotation.RetentionPolicy;
64 import java.util.HashMap;
65 import java.util.List;
66 import java.util.Locale;
67 import java.util.Map;
68 import java.util.concurrent.ExecutionException;
69 import java.util.concurrent.TimeUnit;
70 
71 
72 /**
73  * This class manages all the triggers for Picker syncs.
74  * <p></p>
75  * There are different use cases for triggering a sync:
76  * <p>
77  * 1. Proactive sync - these syncs are proactively performed to minimize the changes that need to be
78  *      synced when the user opens the Photo Picker. The sync should only be performed if the device
79  *      state allows it.
80  * <p>
81  * 2. Reactive sync - these syncs are triggered by the user opening the Photo Picker. These should
82  *      be run immediately since the user is likely to be waiting for the sync response on the UI.
83  */
84 public class PickerSyncManager {
85     private static final String TAG = "SyncWorkManager";
86     public static final int SYNC_LOCAL_ONLY = 1;
87     public static final int SYNC_CLOUD_ONLY = 2;
88     public static final int SYNC_LOCAL_AND_CLOUD = 3;
89     public static final int SYNC_MEDIA_GRANTS = 4;
90 
91     @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD, SYNC_MEDIA_GRANTS })
92     @Retention(RetentionPolicy.SOURCE)
93     public @interface SyncSource {}
94 
95     public static final int SYNC_RESET_MEDIA = 1;
96     public static final int SYNC_RESET_ALBUM = 2;
97 
98     @IntDef(value = {SYNC_RESET_MEDIA, SYNC_RESET_ALBUM})
99     @Retention(RetentionPolicy.SOURCE)
100     public @interface SyncResetType {}
101 
102     /** Clears all search requests and search results from the database. */
103     public static final int SEARCH_RESULTS_FULL_CACHE_RESET = 1;
104     /** Clears search results and suggestions of the local or cloud provider from the database. */
105     public static final int SEARCH_PARTIAL_CACHE_RESET = 2;
106     /** Clears all expired history and cached suggestions from the database. */
107     public static final int EXPIRED_SUGGESTIONS_RESET = 3;
108 
109     @IntDef(value = {
110             SEARCH_RESULTS_FULL_CACHE_RESET,
111             SEARCH_PARTIAL_CACHE_RESET,
112             EXPIRED_SUGGESTIONS_RESET})
113     @Retention(RetentionPolicy.SOURCE)
114     public @interface SearchCacheResetType {}
115 
116     static final String SYNC_WORKER_INPUT_AUTHORITY = "INPUT_AUTHORITY";
117     static final String SYNC_WORKER_INPUT_SYNC_SOURCE = "INPUT_SYNC_TYPE";
118     static final String SYNC_WORKER_INPUT_RESET_TYPE = "INPUT_RESET_TYPE";
119     static final String SYNC_WORKER_INPUT_ALBUM_ID = "INPUT_ALBUM_ID";
120     static final String SYNC_WORKER_INPUT_SEARCH_REQUEST_ID = "INPUT_SEARCH_REQUEST_ID";
121     static final String SYNC_WORKER_INPUT_CATEGORY_ID = "INPUT_CATEGORY_ID";
122     static final String SYNC_WORKER_INPUT_MEDIA_SET_ID = "INPUT_MEDIA_SET_ID";
123     static final String SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID = "INPUT_MEDIA_SET_PICKER_ID";
124     static final String SYNC_WORKER_TAG_IS_PERIODIC = "PERIODIC";
125     static final long PROACTIVE_SYNC_DELAY_MS = 1500;
126     private static final int SYNC_MEDIA_PERIODIC_WORK_INTERVAL = 4; // Time unit is hours.
127     private static final int RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL = 12; // Time unit is hours.
128     // Time unit is days.
129     private static final int RESET_SEARCH_SUGGESTIONS_PERIODIC_WORK_INTERVAL = 1;
130     static final int SEARCH_RESULTS_RESET_DELAY = 30; // Time unit is minutes.
131 
132     public static final String PERIODIC_SYNC_WORK_NAME;
133     private static final String PROACTIVE_LOCAL_SYNC_WORK_NAME;
134     private static final String PROACTIVE_SYNC_WORK_NAME;
135     public static final String IMMEDIATE_LOCAL_SYNC_WORK_NAME;
136     private static final String IMMEDIATE_CLOUD_SYNC_WORK_NAME;
137     public static final String IMMEDIATE_ALBUM_SYNC_WORK_NAME;
138     public static final String IMMEDIATE_LOCAL_SEARCH_SYNC_WORK_NAME;
139     public static final String IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME;
140     public static final String IMMEDIATE_LOCAL_MEDIA_SETS_SYNC_WORK_NAME;
141     public static final String IMMEDIATE_CLOUD_MEDIA_SETS_SYNC_WORK_NAME;
142     public static final String IMMEDIATE_LOCAL_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME;
143     public static final String IMMEDIATE_CLOUD_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME;
144     public static final String SEARCH_CACHE_RESET_WORK_NAME;
145     public static final String PERIODIC_SEARCH_SUGGESTIONS_RESET_WORK_NAME;
146     public static final String PERIODIC_ALBUM_RESET_WORK_NAME;
147     private static final String ENDLESS_WORK_NAME;
148     public static final String IMMEDIATE_GRANTS_SYNC_WORK_NAME;
149     public static final String SHOULD_SYNC_GRANTS;
150     public static final String EXTRA_MIME_TYPES;
151 
152     static {
153         final String syncPeriodicPrefix = "SYNC_MEDIA_PERIODIC_";
154         final String syncProactivePrefix = "SYNC_MEDIA_PROACTIVE_";
155         final String syncImmediatePrefix = "SYNC_MEDIA_IMMEDIATE_";
156         final String syncSearchResultsImmediatePrefix = "SYNC_SEARCH_RESULTS_IMMEDIATE_";
157         final String syncMediaSetsImmediatePrefix = "SYNC_MEDIA_SETS_IMMEDIATE_";
158         final String syncMediaInMediaSetImmediatePrefix = "SYNC_MEDIA_IN_MEDIA_SET_IMMEDIATE";
159         final String syncAllSuffix = "ALL";
160         final String syncLocalSuffix = "LOCAL";
161         final String syncCloudSuffix = "CLOUD";
162         final String syncGrantsSuffix = "GRANTS";
163 
164         PERIODIC_ALBUM_RESET_WORK_NAME = "RESET_ALBUM_MEDIA_PERIODIC";
165         PERIODIC_SYNC_WORK_NAME = syncPeriodicPrefix + syncAllSuffix;
166         PROACTIVE_LOCAL_SYNC_WORK_NAME = syncProactivePrefix + syncLocalSuffix;
167         PROACTIVE_SYNC_WORK_NAME = syncProactivePrefix + syncAllSuffix;
168         IMMEDIATE_GRANTS_SYNC_WORK_NAME = syncImmediatePrefix + syncGrantsSuffix;
169         IMMEDIATE_LOCAL_SYNC_WORK_NAME = syncImmediatePrefix + syncLocalSuffix;
170         IMMEDIATE_CLOUD_SYNC_WORK_NAME = syncImmediatePrefix + syncCloudSuffix;
171         IMMEDIATE_ALBUM_SYNC_WORK_NAME = "SYNC_ALBUM_MEDIA_IMMEDIATE";
172         IMMEDIATE_LOCAL_SEARCH_SYNC_WORK_NAME = syncSearchResultsImmediatePrefix + syncLocalSuffix;
173         // Use this work name to schedule cloud search results sync and cloud search results
174         // reset both.
175         IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME = syncSearchResultsImmediatePrefix + syncCloudSuffix;
176         IMMEDIATE_LOCAL_MEDIA_SETS_SYNC_WORK_NAME = syncMediaSetsImmediatePrefix + syncLocalSuffix;
177         IMMEDIATE_CLOUD_MEDIA_SETS_SYNC_WORK_NAME = syncMediaSetsImmediatePrefix + syncCloudSuffix;
178         IMMEDIATE_LOCAL_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME =
179                 syncMediaInMediaSetImmediatePrefix + syncLocalSuffix;
180         IMMEDIATE_CLOUD_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME =
181                 syncMediaInMediaSetImmediatePrefix + syncCloudSuffix;
182         SEARCH_CACHE_RESET_WORK_NAME = "SEARCH_CACHE_FULL_RESET";
183         PERIODIC_SEARCH_SUGGESTIONS_RESET_WORK_NAME = "RESET_SEARCH_SUGGESTIONS";
184         ENDLESS_WORK_NAME = "ENDLESS_WORK";
185         SHOULD_SYNC_GRANTS = "SHOULD_SYNC_GRANTS";
186         EXTRA_MIME_TYPES = "mime_types";
187     }
188 
189     private final WorkManager mWorkManager;
190     private final Context mContext;
191 
PickerSyncManager(@onNull WorkManager workManager, @NonNull Context context)192     public PickerSyncManager(@NonNull WorkManager workManager, @NonNull Context context) {
193         mWorkManager = requireNonNull(workManager);
194         mContext = requireNonNull(context);
195     }
196 
197     /**
198      * Schedule proactive periodic media syncs.
199      *
200      * @param configStore And instance of {@link ConfigStore} that holds all config info.
201      */
schedulePeriodicSync(@onNull ConfigStore configStore)202     public void schedulePeriodicSync(@NonNull ConfigStore configStore) {
203         schedulePeriodicSync(configStore, /* periodicSyncInitialDelay */10000L);
204     }
205 
206     /**
207      * Schedule proactive periodic media syncs.
208      *
209      * @param configStore And instance of {@link ConfigStore} that holds all the config info.
210      * @param periodicSyncInitialDelay Initial delay of periodic sync in milliseconds.
211      */
schedulePeriodicSync( @onNull ConfigStore configStore, long periodicSyncInitialDelay)212     public void schedulePeriodicSync(
213             @NonNull ConfigStore configStore,
214             long periodicSyncInitialDelay) {
215         requireNonNull(configStore);
216 
217         // Move to a background thread to remove from MediaProvider boot path.
218         BackgroundThread.getHandler().postDelayed(
219                 () -> {
220                     try {
221                         setUpEndlessWork();
222                         setUpPeriodicWork(configStore);
223                     } catch (RuntimeException e) {
224                         Log.e(TAG, "Could not schedule workers", e);
225                     }
226                 },
227                 periodicSyncInitialDelay
228         );
229 
230         // Subscribe to device config changes so we can enable periodic workers if Cloud
231         // Photopicker is enabled.
232         configStore.addOnChangeListener(
233                 BackgroundThread.getExecutor(),
234                 () -> setUpPeriodicWork(configStore));
235     }
236 
237     /**
238      * Will register new unique {@link Worker} for periodic sync and picker database maintenance if
239      * the cloud photopicker experiment is currently enabled.
240      */
setUpPeriodicWork(@onNull ConfigStore configStore)241     private void setUpPeriodicWork(@NonNull ConfigStore configStore) {
242         try {
243             requireNonNull(configStore);
244 
245             if (configStore.isCloudMediaInPhotoPickerEnabled()) {
246                 PickerSyncNotificationHelper.createNotificationChannel(mContext);
247 
248                 schedulePeriodicSyncs();
249                 schedulePeriodicAlbumReset();
250             } else {
251                 // Disable any scheduled ongoing work if the feature is disabled.
252                 mWorkManager.cancelUniqueWork(PERIODIC_SYNC_WORK_NAME);
253                 mWorkManager.cancelUniqueWork(PERIODIC_ALBUM_RESET_WORK_NAME);
254             }
255 
256             if (Flags.enablePhotopickerSearch()) {
257                 schedulePeriodicSearchSuggestionsReset();
258             } else {
259                 mWorkManager.cancelUniqueWork(PERIODIC_SEARCH_SUGGESTIONS_RESET_WORK_NAME);
260             }
261         } catch (RuntimeException e) {
262             Log.e(TAG, "Could not schedule periodic work", e);
263         }
264     }
265 
266     /**
267      * Will register a new {@link Worker} for 1 year in the future. This is to prevent the {@link
268      * androidx.work.impl.background.systemalarm.RescheduleReceiver} from being disabled by WM
269      * internals, which triggers PACKAGE_CHANGED broadcasts every time a new worker is scheduled. As
270      * a work around to prevent these broadcasts, we enqueue a worker here very far in the future to
271      * prevent the component from being disabled by work manager.
272      *
273      * <p>{@see b/314863434 for additional context.}
274      */
setUpEndlessWork()275     private void setUpEndlessWork() {
276 
277         OneTimeWorkRequest request =
278                 new OneTimeWorkRequest.Builder(EndlessWorker.class)
279                         .setInitialDelay(365, TimeUnit.DAYS)
280                         .build();
281 
282         mWorkManager.enqueueUniqueWork(
283                 ENDLESS_WORK_NAME, ExistingWorkPolicy.KEEP, request);
284         Log.d(TAG, "EndlessWorker has been enqueued");
285     }
286 
schedulePeriodicSyncs()287     private void schedulePeriodicSyncs() {
288         Log.i(TAG, "Scheduling periodic proactive syncs");
289 
290         final Data inputData =
291                 new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
292         final PeriodicWorkRequest periodicSyncRequest = getPeriodicProactiveSyncRequest(inputData);
293 
294         try {
295             // Note that the first execution of periodic work happens immediately or as soon as the
296             // given Constraints are met.
297             final Operation enqueueOperation = mWorkManager
298                     .enqueueUniquePeriodicWork(
299                             PERIODIC_SYNC_WORK_NAME,
300                             ExistingPeriodicWorkPolicy.KEEP,
301                             periodicSyncRequest
302                     );
303 
304             // Check that the request has been successfully enqueued.
305             enqueueOperation.getResult().get();
306         } catch (InterruptedException | ExecutionException e) {
307             Log.e(TAG, "Could not enqueue periodic proactive picker sync request", e);
308         }
309     }
310 
schedulePeriodicAlbumReset()311     private void schedulePeriodicAlbumReset() {
312         Log.i(TAG, "Scheduling periodic picker album data resets");
313 
314         final Data inputData =
315                 new Data(
316                         Map.of(
317                                 SYNC_WORKER_INPUT_SYNC_SOURCE,
318                                 SYNC_LOCAL_AND_CLOUD,
319                                 SYNC_WORKER_INPUT_RESET_TYPE,
320                                 SYNC_RESET_ALBUM));
321         final PeriodicWorkRequest periodicAlbumResetRequest =
322                 getPeriodicAlbumResetRequest(inputData);
323 
324         try {
325             // Note that the first execution of periodic work happens immediately or as soon
326             // as the given Constraints are met.
327             Operation enqueueOperation =
328                     mWorkManager.enqueueUniquePeriodicWork(
329                             PERIODIC_ALBUM_RESET_WORK_NAME,
330                             ExistingPeriodicWorkPolicy.KEEP,
331                             periodicAlbumResetRequest);
332 
333             // Check that the request has been successfully enqueued.
334             enqueueOperation.getResult().get();
335         } catch (InterruptedException | ExecutionException e) {
336             Log.e(TAG, "Could not enqueue periodic picker album resets request", e);
337         }
338     }
339 
340     /**
341      * Use this method for proactive syncs. The sync might take a while to start. Some device state
342      * conditions may apply before the sync can start like battery level etc.
343      *
344      * @param localOnly - whether the proactive sync should only sync with the local provider.
345      */
syncMediaProactively(Boolean localOnly)346     public void syncMediaProactively(Boolean localOnly) {
347 
348         final int syncSource = localOnly ? SYNC_LOCAL_ONLY : SYNC_LOCAL_AND_CLOUD;
349         final String workName =
350                 localOnly ? PROACTIVE_LOCAL_SYNC_WORK_NAME : PROACTIVE_SYNC_WORK_NAME;
351 
352         final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
353         final OneTimeWorkRequest syncRequest = getOneTimeProactiveSyncRequest(inputData);
354 
355         // Don't wait for the sync operation to enqueue so that Picker sync enqueue
356         // requests in order to avoid adding latency to critical MP code paths.
357         mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, syncRequest);
358     }
359 
360     /**
361      * Use this method for reactive syncs which are user triggered.
362      *
363      * @param pickerSyncRequestExtras extras used to figure out which all syncs to trigger.
364      */
syncMediaImmediately( @onNull PickerSyncRequestExtras pickerSyncRequestExtras, @NonNull ConfigStore configStore)365     public void syncMediaImmediately(
366             @NonNull PickerSyncRequestExtras pickerSyncRequestExtras,
367             @NonNull ConfigStore configStore) {
368         requireNonNull(pickerSyncRequestExtras);
369         requireNonNull(configStore);
370 
371         if (configStore.isModernPickerEnabled()) {
372             // sync for grants is only required for the modern picker, the java picker uses
373             // MediaStore to directly fetch the grants for all purposes of selection.
374             syncGrantsImmediately(
375                     IMMEDIATE_GRANTS_SYNC_WORK_NAME,
376                     pickerSyncRequestExtras.getCallingPackageUid(),
377                     pickerSyncRequestExtras.isShouldSyncGrants(),
378                     pickerSyncRequestExtras.getMimeTypes());
379         }
380 
381         syncMediaImmediately(PickerSyncManager.SYNC_LOCAL_ONLY, IMMEDIATE_LOCAL_SYNC_WORK_NAME);
382         if (!pickerSyncRequestExtras.shouldSyncLocalOnlyData()) {
383             syncMediaImmediately(PickerSyncManager.SYNC_CLOUD_ONLY, IMMEDIATE_CLOUD_SYNC_WORK_NAME);
384         }
385     }
386 
387     /**
388      * Use this method for reactive syncs for grants from the external database.
389      */
syncGrantsImmediately(@onNull String workName, int callingPackageUid, boolean shouldSyncGrants, String[] mimeTypes)390     private void syncGrantsImmediately(@NonNull String workName, int callingPackageUid,
391             boolean shouldSyncGrants, String[] mimeTypes) {
392         final Data inputData = new Data(
393                 Map.of(
394                         Intent.EXTRA_UID, callingPackageUid,
395                         SHOULD_SYNC_GRANTS, shouldSyncGrants,
396                         EXTRA_MIME_TYPES, mimeTypes
397                 )
398         );
399 
400         final OneTimeWorkRequest syncRequestForGrants =
401                 buildOneTimeWorkerRequest(ImmediateGrantsSyncWorker.class, inputData);
402 
403         // Track the new sync request(s)
404         trackNewSyncRequests(PickerSyncManager.SYNC_MEDIA_GRANTS, syncRequestForGrants.getId());
405 
406         // Enqueue grants sync request
407         try {
408             final Operation enqueueOperation = mWorkManager
409                     .enqueueUniqueWork(workName, ExistingWorkPolicy.APPEND_OR_REPLACE,
410                             syncRequestForGrants);
411 
412             // Check that the request has been successfully enqueued.
413             enqueueOperation.getResult().get();
414         } catch (Exception e) {
415             Log.e(TAG, "Could not enqueue expedited picker grants sync request", e);
416             markSyncAsComplete(PickerSyncManager.SYNC_MEDIA_GRANTS,
417                     syncRequestForGrants.getId());
418         }
419     }
420 
421     /**
422      * Use this method for reactive syncs with either, local and cloud providers, or both.
423      */
syncMediaImmediately(@yncSource int syncSource, @NonNull String workName)424     private void syncMediaImmediately(@SyncSource int syncSource, @NonNull String workName) {
425         final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
426         final OneTimeWorkRequest syncRequest =
427                 buildOneTimeWorkerRequest(ImmediateSyncWorker.class, inputData);
428 
429         // Track the new sync request(s)
430         trackNewSyncRequests(syncSource, syncRequest.getId());
431 
432         // Enqueue local or cloud sync request
433         try {
434             final Operation enqueueOperation = mWorkManager
435                     .enqueueUniqueWork(workName, ExistingWorkPolicy.APPEND_OR_REPLACE, syncRequest);
436 
437             // Check that the request has been successfully enqueued.
438             enqueueOperation.getResult().get();
439         } catch (Exception e) {
440             Log.e(TAG, "Could not enqueue expedited picker sync request", e);
441             markSyncAsComplete(syncSource, syncRequest.getId());
442         }
443     }
444 
445     /**
446      * Use this method for reactive syncs which are user action triggered.
447      *
448      * @param albumId is the id of the album that needs to be synced.
449      * @param authority The authority of the album media.
450      * @param isLocal is {@code true} iff the album authority is of the local provider.
451      */
syncAlbumMediaForProviderImmediately( @onNull String albumId, @NonNull String authority, boolean isLocal)452     public void syncAlbumMediaForProviderImmediately(
453             @NonNull String albumId, @NonNull String authority, boolean isLocal) {
454         syncAlbumMediaForProviderImmediately(albumId, getSyncSource(isLocal), authority);
455     }
456 
457     /**
458      * Use this method for reactive syncs which are user action triggered.
459      *
460      * @param albumId is the id of the album that needs to be synced.
461      * @param syncSource indicates if the sync is required with local provider or cloud provider or
462      *     both.
463      */
syncAlbumMediaForProviderImmediately( @onNull String albumId, @SyncSource int syncSource, String authority)464     private void syncAlbumMediaForProviderImmediately(
465             @NonNull String albumId, @SyncSource int syncSource, String authority) {
466         final Data inputData =
467                 new Data(
468                         Map.of(
469                                 SYNC_WORKER_INPUT_AUTHORITY, authority,
470                                 SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource,
471                                 SYNC_WORKER_INPUT_RESET_TYPE, SYNC_RESET_ALBUM,
472                                 SYNC_WORKER_INPUT_ALBUM_ID, albumId));
473         final OneTimeWorkRequest resetRequest =
474                 buildOneTimeWorkerRequest(MediaResetWorker.class, inputData);
475         final OneTimeWorkRequest syncRequest =
476                 buildOneTimeWorkerRequest(ImmediateAlbumSyncWorker.class, inputData);
477 
478         // Track the new sync request(s)
479         trackNewAlbumMediaSyncRequests(syncSource, resetRequest.getId());
480         trackNewAlbumMediaSyncRequests(syncSource, syncRequest.getId());
481 
482         // Enqueue local or cloud sync requests
483         try {
484             final Operation enqueueOperation =
485                     mWorkManager
486                             .beginUniqueWork(
487                                     IMMEDIATE_ALBUM_SYNC_WORK_NAME,
488                                     ExistingWorkPolicy.APPEND_OR_REPLACE,
489                                     resetRequest)
490                             .then(syncRequest).enqueue();
491 
492             // Check that the request has been successfully enqueued.
493             enqueueOperation.getResult().get();
494         } catch (Exception e) {
495             Log.e(TAG, "Could not enqueue expedited picker sync request", e);
496             markAlbumMediaSyncAsComplete(syncSource, resetRequest.getId());
497             markAlbumMediaSyncAsComplete(syncSource, syncRequest.getId());
498         }
499     }
500 
501     /**
502      * Use this method for reactive search results sync which are user action triggered.
503      *
504      * @param searchRequestId Identifier for the search request.
505      * @param syncSource indicates if the sync is required with local provider or cloud provider.
506      *                   Sync source cannot be both in this case.
507      * @param authority Authority of the provider.
508      */
syncSearchResultsForProvider( int searchRequestId, @SyncSource int syncSource, String authority)509     public void syncSearchResultsForProvider(
510             int searchRequestId, @SyncSource int syncSource, String authority) {
511         final Data inputData =
512                 new Data(
513                         Map.of(
514                                 SYNC_WORKER_INPUT_AUTHORITY, authority,
515                                 SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource,
516                                 SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, searchRequestId));
517 
518         final String workName = syncSource == SYNC_LOCAL_ONLY
519                 ? IMMEDIATE_LOCAL_SEARCH_SYNC_WORK_NAME
520                 : IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME;
521 
522         final String tag = String.format(Locale.ROOT, "%s-%s-%s",
523                 workName, authority, searchRequestId);
524 
525         final OneTimeWorkRequest syncRequest =
526                 new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
527                         .setInputData(inputData)
528                         .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
529                         .addTag(tag)
530                         .build();
531 
532         synchronized (PickerSyncManager.class) {
533             // Check if this work is already in progress. This logic is inside a class level
534             // synchronized block to avoid race conditions.
535             try {
536                 if (isWorkPendingForTag(tag)) {
537                     Log.d(TAG, "Sync work is already in progress. Ignoring sync request " + tag);
538                     return;
539                 }
540             } catch (InterruptedException | ExecutionException | RuntimeException e) {
541                 Log.e(TAG, "Error occurred in fetching work info - scheduling sync work " + tag);
542             }
543 
544             // Clear all existing requests since there can be only one unique work running and our
545             // new sync work will replace the existing work (if any).
546             markAllSearchResultsSyncAsComplete(syncSource);
547 
548             // Track the new sync request
549             trackNewSearchResultsSyncRequests(syncSource, syncRequest.getId());
550 
551             // Enqueue local or cloud sync request
552             try {
553                 final Operation enqueueOperation = mWorkManager.enqueueUniqueWork(
554                         workName,
555                         ExistingWorkPolicy.REPLACE,
556                         syncRequest);
557 
558                 // Check that the request has been successfully enqueued.
559                 enqueueOperation.getResult().get();
560             } catch (Exception e) {
561                 Log.e(TAG, "Could not enqueue expedited search results sync request", e);
562                 markSearchResultsSyncAsComplete(syncSource, syncRequest.getId());
563             }
564         }
565     }
566 
567     /**
568      * Schedules work to reset all cloud search results and suggestions synced in the database.
569      * This is used when the cloud media provider changes or the collection id of the cloud media
570      * provider changes indicating that a full reset of cloud media is required.
571      *
572      * @param cloudAuthority Cloud authority might be null if there was an error in getting it.
573      */
resetCloudSearchCache(@ullable String cloudAuthority)574     public void resetCloudSearchCache(@Nullable String cloudAuthority) {
575         final Map<String, Object> inputMap = new HashMap<>();
576         inputMap.put(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY);
577         inputMap.put(SYNC_WORKER_INPUT_RESET_TYPE, SEARCH_PARTIAL_CACHE_RESET);
578         if (cloudAuthority != null) {
579             inputMap.put(SYNC_WORKER_INPUT_AUTHORITY, cloudAuthority);
580         }
581         final Data inputData = new Data(inputMap);
582 
583         final OneTimeWorkRequest syncRequest =
584                 buildOneTimeWorkerRequest(SearchResetWorker.class, inputData);
585 
586         try {
587             Log.d(TAG, "Scheduling cloud search results reset request.");
588 
589             // Enqueue cloud search reset request with the ExistingWorkPolicy as REPLACE so
590             // that any currently running synced will be cancelled. Don't wait to check the
591             // results of the enqueue operation because this runs the critical path.
592             mWorkManager.enqueueUniqueWork(
593                     IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME,
594                     ExistingWorkPolicy.REPLACE,
595                     syncRequest);
596         } catch (Exception e) {
597             Log.e(TAG, "Could not enqueue search results cloud reset request", e);
598         }
599     }
600 
601     /**
602      * Schedules work to reset all search results cache after some delay from the search database.
603      */
delayedResetSearchCache()604     public void delayedResetSearchCache() {
605         final Data inputData =
606                 new Data(Map.of(
607                         SYNC_WORKER_INPUT_RESET_TYPE, SEARCH_RESULTS_FULL_CACHE_RESET,
608                         SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
609         final OneTimeWorkRequest syncRequest =
610                 getDelayedSearchResetRequest(inputData);
611 
612         // Enqueue full cache reset request. Ensure that this runs when the device is idle to
613         // prevent search requests from clearing when the user is using PhotoPicker search feature.
614         try {
615             Log.d(TAG, "Scheduling delayed search results full cache reset request.");
616             mWorkManager.enqueueUniqueWork(
617                     SEARCH_CACHE_RESET_WORK_NAME,
618                     ExistingWorkPolicy.KEEP,
619                     syncRequest);
620         } catch (Exception e) {
621             Log.e(TAG, "Could not enqueue search results full cache reset request", e);
622         }
623     }
624 
625     /**
626      * Schedules periodic syncs that clears expired search history and cached suggestions from the
627      * Picker database when the search feature is turned on.
628      */
schedulePeriodicSearchSuggestionsReset()629     public void schedulePeriodicSearchSuggestionsReset() {
630         final Data inputData =
631                 new Data(Map.of(
632                         SYNC_WORKER_INPUT_RESET_TYPE, EXPIRED_SUGGESTIONS_RESET,
633                         SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
634         final PeriodicWorkRequest syncRequest =
635                 getPeriodicSearchSuggestionsResetRequest(inputData);
636 
637         try {
638             Operation enqueueOperation =
639                     mWorkManager.enqueueUniquePeriodicWork(
640                             PERIODIC_SEARCH_SUGGESTIONS_RESET_WORK_NAME,
641                             ExistingPeriodicWorkPolicy.KEEP,
642                             syncRequest);
643 
644             // Check that the request has been successfully enqueued.
645             enqueueOperation.getResult().get();
646         } catch (InterruptedException | ExecutionException e) {
647             Log.e(TAG, "Could not enqueue periodic search suggestions request", e);
648         }
649     }
650 
651     /**
652      * Creates OneTimeWork request for syncing media sets with the given provider.
653      * The existing media sets cache and the media sets content cache for the given categoryId
654      * is cleared before a new media sets sync is triggered to ensure accuracy of the media sets
655      * metadata stored in the database. The reset cache and sync requests are chained to ensure
656      * correctness of the entire operation.
657      * @param requestParams The MediaSetsSyncRequestsParams object containing all input parameters
658      *                      for creating a sync request
659      * @param syncSource Indicates whether the sync is required with the local provider or
660      *                   the cloud provider.
661      */
syncMediaSetsForProvider( MediaSetsSyncRequestParams requestParams, @SyncSource int syncSource)662     public void syncMediaSetsForProvider(
663             MediaSetsSyncRequestParams requestParams, @SyncSource int syncSource) {
664         // Create media sets sync request
665         final Map<String, Object> syncRequestInputMap = new HashMap<>();
666         syncRequestInputMap.put(SYNC_WORKER_INPUT_AUTHORITY, requestParams.getAuthority());
667         syncRequestInputMap.put(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource);
668         syncRequestInputMap.put(SYNC_WORKER_INPUT_CATEGORY_ID, requestParams.getCategoryId());
669         if (requestParams.getMimeTypes() != null) {
670             syncRequestInputMap.put(EXTRA_MIME_TYPES, requestParams.getMimeTypes().toArray(
671                     new String[0]
672             ));
673         }
674         final Data syncRequestInputData = new Data(syncRequestInputMap);
675         final OneTimeWorkRequest syncRequest =
676                 buildOneTimeWorkerRequest(MediaSetsSyncWorker.class, syncRequestInputData);
677 
678         // Create media sets reset request. MediaSets sync are non-resumable.
679         // It's fine to delete the entire cache before a new set is triggered for the given
680         // categoryId.
681         // The media sets content cache for media sets belonging to the given categoryId
682         // is also cleared before we start syncing any particular media set for its content.
683         // These tables are cleared once per picker session before the media sets sync for this
684         // session is triggered. This ensures that the data read from the cache in every session
685         // is always in sync with the cloud provider.
686         final Map<String, Object> resetRequestInputMap = new HashMap<>();
687         resetRequestInputMap.put(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource);
688         resetRequestInputMap.put(SYNC_WORKER_INPUT_CATEGORY_ID, requestParams.getCategoryId());
689         resetRequestInputMap.put(SYNC_WORKER_INPUT_AUTHORITY, requestParams.getAuthority());
690         final Data resetRequestInputData = new Data(resetRequestInputMap);
691         final OneTimeWorkRequest resetRequest =
692                 buildOneTimeWorkerRequest(MediaSetsResetWorker.class, resetRequestInputData);
693 
694         // Track the new requests
695         trackNewMediaSetsSyncRequest(syncSource, resetRequest.getId());
696         trackNewMediaSetsSyncRequest(syncSource, syncRequest.getId());
697 
698         final String workName = syncSource == SYNC_LOCAL_ONLY
699                 ? IMMEDIATE_LOCAL_MEDIA_SETS_SYNC_WORK_NAME
700                 : IMMEDIATE_CLOUD_MEDIA_SETS_SYNC_WORK_NAME;
701         // Enqueue local or cloud sync request
702         try {
703             final Operation enqueueOperation = mWorkManager
704                     .beginUniqueWork(
705                             workName,
706                             ExistingWorkPolicy.APPEND_OR_REPLACE,
707                             resetRequest)
708                     .then(syncRequest).enqueue();
709 
710             // Check that the request has been successfully enqueued.
711             enqueueOperation.getResult().get();
712         } catch (Exception e) {
713             Log.e(TAG, "Could not enqueue expedited media sets sync request", e);
714             markMediaSetsSyncAsComplete(syncSource, resetRequest.getId());
715             markMediaSetsSyncAsComplete(syncSource, syncRequest.getId());
716         }
717     }
718 
719     /**
720      * Creates OneTimeWork request for syncing media in media set with the given provider
721      * @param requestParams The MediaInMediaSetSyncRequestParams object containing all input
722      *                      parameters for creating a sync request
723      * @param syncSource Indicates whether the sync is required with the local provider or
724      *                   the cloud provider.
725      */
syncMediaInMediaSetForProvider( MediaInMediaSetSyncRequestParams requestParams, @SyncSource int syncSource)726     public void syncMediaInMediaSetForProvider(
727             MediaInMediaSetSyncRequestParams requestParams,
728             @SyncSource int syncSource) {
729         final Data inputData =
730                 new Data(
731                         Map.of(
732                                 SYNC_WORKER_INPUT_AUTHORITY, requestParams.getAuthority(),
733                                 SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource,
734                                 SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID,
735                                 requestParams.getMediaSetPickerId()));
736 
737         final String workName = syncSource == SYNC_LOCAL_ONLY
738                 ? IMMEDIATE_LOCAL_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME
739                 : IMMEDIATE_CLOUD_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME;
740 
741         final String tag = String.format(Locale.ROOT, "%s-%s-%s",
742                 workName, requestParams.getAuthority(), requestParams.getMediaSetPickerId());
743 
744         final OneTimeWorkRequest syncRequest =
745                 new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
746                         .setInputData(inputData)
747                         .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
748                         .addTag(tag)
749                         .build();
750 
751         synchronized (PickerSyncManager.class) {
752             // Check if this work is already in progress. This logic is inside a class level
753             // synchronized block to avoid race conditions.
754             try {
755                 if (isWorkPendingForTag(tag)) {
756                     Log.d(TAG, "Sync work is already in progress. Ignoring sync request " + tag);
757                     return;
758                 }
759             } catch (InterruptedException | ExecutionException | RuntimeException e) {
760                 Log.e(TAG, "Error occurred in fetching work info - scheduling sync work " + tag);
761             }
762 
763             markAllMediaInMediaSetsSyncAsComplete(syncSource);
764 
765             // track the new request
766             trackNewMediaInMediaSetSyncRequest(syncSource, syncRequest.getId());
767 
768             // Enqueue local or cloud sync request
769             try {
770                 final Operation enqueueOperation = mWorkManager.enqueueUniqueWork(
771                         workName,
772                         ExistingWorkPolicy.REPLACE,
773                         syncRequest
774                 );
775 
776                 // Check that the request has been successfully enqueued.
777                 enqueueOperation.getResult().get();
778             } catch (Exception e) {
779                 Log.e(TAG, "Could not enqueue expedited media in media set sync request", e);
780                 markMediaInMediaSetSyncAsComplete(syncSource, syncRequest.getId());
781             }
782         }
783     }
784 
isWorkPendingForTag(@onNull String tag)785     private boolean isWorkPendingForTag(@NonNull String tag)
786             throws InterruptedException, ExecutionException {
787         ListenableFuture<List<WorkInfo>> future = mWorkManager.getWorkInfosByTag(tag);
788         List<WorkInfo> workInfos = future.get();
789         for (WorkInfo workInfo : workInfos) {
790             if (!workInfo.getState().isFinished()) {
791                 return true;
792             }
793         }
794         return false;
795     }
796 
797     @NonNull
buildOneTimeWorkerRequest( @onNull Class<? extends Worker> workerClass, Data inputData)798     private OneTimeWorkRequest buildOneTimeWorkerRequest(
799             @NonNull Class<? extends Worker> workerClass, Data inputData) {
800         if (inputData != null) {
801             return new OneTimeWorkRequest.Builder(workerClass)
802                     .setInputData(inputData)
803                     .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
804                     .build();
805         } else {
806             return new OneTimeWorkRequest.Builder(workerClass)
807                     .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
808                     .build();
809         }
810     }
811 
812     @NonNull
getPeriodicProactiveSyncRequest(@onNull Data inputData)813     private PeriodicWorkRequest getPeriodicProactiveSyncRequest(@NonNull Data inputData) {
814         return new PeriodicWorkRequest.Builder(
815                 ProactiveSyncWorker.class, SYNC_MEDIA_PERIODIC_WORK_INTERVAL, TimeUnit.HOURS)
816                 .setInputData(inputData)
817                 .setConstraints(getRequiresChargingAndIdleConstraints())
818                 .build();
819     }
820 
821     @NonNull
getPeriodicAlbumResetRequest(@onNull Data inputData)822     private PeriodicWorkRequest getPeriodicAlbumResetRequest(@NonNull Data inputData) {
823 
824         return new PeriodicWorkRequest.Builder(
825                         MediaResetWorker.class,
826                         RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL,
827                         TimeUnit.HOURS)
828                 .setInputData(inputData)
829                 .setConstraints(getRequiresChargingAndIdleConstraints())
830                 .addTag(SYNC_WORKER_TAG_IS_PERIODIC)
831                 .build();
832     }
833 
834     /**
835      * @param inputData Input data required by the Worker.
836      * @return A PeriodicWorkRequest for periodically clearing expired search suggestions from
837      * the database.
838      */
839     @NonNull
getPeriodicSearchSuggestionsResetRequest(@onNull Data inputData)840     private PeriodicWorkRequest getPeriodicSearchSuggestionsResetRequest(@NonNull Data inputData) {
841 
842         return new PeriodicWorkRequest.Builder(
843                 SearchResetWorker.class,
844                 RESET_SEARCH_SUGGESTIONS_PERIODIC_WORK_INTERVAL,
845                 TimeUnit.DAYS)
846                 .setInputData(inputData)
847                 .setConstraints(getRequiresChargingAndIdleConstraints())
848                 .build();
849     }
850 
851     /**
852      * @param inputData Input data required by the Worker.
853      * @return A OneTimeWorkRequest for clearing all search results cache after an initial delay.
854      */
855     @NonNull
getDelayedSearchResetRequest(@onNull Data inputData)856     private OneTimeWorkRequest getDelayedSearchResetRequest(@NonNull Data inputData) {
857         Constraints constraints =  new Constraints.Builder()
858                 .setRequiresDeviceIdle(true)
859                 .build();
860 
861         return new OneTimeWorkRequest
862                 .Builder(SearchResetWorker.class)
863                 .setConstraints(constraints)
864                 .setInitialDelay(SEARCH_RESULTS_RESET_DELAY, TimeUnit.MINUTES)
865                 .setInputData(inputData)
866                 .build();
867     }
868 
869     @NonNull
getOneTimeProactiveSyncRequest(@onNull Data inputData)870     private OneTimeWorkRequest getOneTimeProactiveSyncRequest(@NonNull Data inputData) {
871         Constraints constraints =  new Constraints.Builder()
872                 .setRequiresBatteryNotLow(true)
873                 .build();
874 
875         return new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
876                 .setInputData(inputData)
877                 .setConstraints(constraints)
878                 .setInitialDelay(PROACTIVE_SYNC_DELAY_MS, TimeUnit.MILLISECONDS)
879                 .build();
880     }
881 
882     @NonNull
getRequiresChargingAndIdleConstraints()883     private static Constraints getRequiresChargingAndIdleConstraints() {
884         return new Constraints.Builder()
885                 .setRequiresCharging(true)
886                 .setRequiresDeviceIdle(true)
887                 .build();
888     }
889 
890     @SyncSource
getSyncSource(boolean isLocal)891     private static int getSyncSource(boolean isLocal) {
892         return isLocal
893                 ? SYNC_LOCAL_ONLY
894                 : SYNC_CLOUD_ONLY;
895     }
896 }
897