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.markSyncAsComplete;
21 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewAlbumMediaSyncRequests;
22 import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewSyncRequests;
23 
24 import static java.util.Objects.requireNonNull;
25 
26 import android.content.Context;
27 import android.util.Log;
28 
29 import androidx.annotation.IntDef;
30 import androidx.annotation.NonNull;
31 import androidx.work.Constraints;
32 import androidx.work.Data;
33 import androidx.work.ExistingPeriodicWorkPolicy;
34 import androidx.work.ExistingWorkPolicy;
35 import androidx.work.OneTimeWorkRequest;
36 import androidx.work.Operation;
37 import androidx.work.OutOfQuotaPolicy;
38 import androidx.work.PeriodicWorkRequest;
39 import androidx.work.WorkManager;
40 import androidx.work.Worker;
41 
42 import com.android.modules.utils.BackgroundThread;
43 import com.android.providers.media.ConfigStore;
44 
45 import org.jetbrains.annotations.NotNull;
46 
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.util.Map;
50 import java.util.concurrent.ExecutionException;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * This class manages all the triggers for Picker syncs.
55  * <p></p>
56  * There are different use cases for triggering a sync:
57  * <p>
58  * 1. Proactive sync - these syncs are proactively performed to minimize the changes that need to be
59  *      synced when the user opens the Photo Picker. The sync should only be performed if the device
60  *      state allows it.
61  * <p>
62  * 2. Reactive sync - these syncs are triggered by the user opening the Photo Picker. These should
63  *      be run immediately since the user is likely to be waiting for the sync response on the UI.
64  */
65 public class PickerSyncManager {
66     private static final String TAG = "SyncWorkManager";
67     public static final int SYNC_LOCAL_ONLY = 1;
68     public static final int SYNC_CLOUD_ONLY = 2;
69     public static final int SYNC_LOCAL_AND_CLOUD = 3;
70 
71     @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD })
72     @Retention(RetentionPolicy.SOURCE)
73     public @interface SyncSource {}
74 
75     public static final int SYNC_RESET_MEDIA = 1;
76     public static final int SYNC_RESET_ALBUM = 2;
77 
78     @IntDef(value = {SYNC_RESET_MEDIA, SYNC_RESET_ALBUM})
79     @Retention(RetentionPolicy.SOURCE)
80     public @interface SyncResetType {}
81 
82     static final String SYNC_WORKER_INPUT_AUTHORITY = "INPUT_AUTHORITY";
83     static final String SYNC_WORKER_INPUT_SYNC_SOURCE = "INPUT_SYNC_TYPE";
84     static final String SYNC_WORKER_INPUT_RESET_TYPE = "INPUT_RESET_TYPE";
85     static final String SYNC_WORKER_INPUT_ALBUM_ID = "INPUT_ALBUM_ID";
86     static final String SYNC_WORKER_TAG_IS_PERIODIC = "PERIODIC";
87     static final long PROACTIVE_SYNC_DELAY_MS = 1500;
88     private static final int SYNC_MEDIA_PERIODIC_WORK_INTERVAL = 4; // Time unit is hours.
89     private static final int RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL = 12; // Time unit is hours.
90 
91     public static final String PERIODIC_SYNC_WORK_NAME;
92     private static final String PROACTIVE_LOCAL_SYNC_WORK_NAME;
93     private static final String PROACTIVE_SYNC_WORK_NAME;
94     public static final String IMMEDIATE_LOCAL_SYNC_WORK_NAME;
95     private static final String IMMEDIATE_CLOUD_SYNC_WORK_NAME;
96     public static final String IMMEDIATE_ALBUM_SYNC_WORK_NAME;
97     public static final String PERIODIC_ALBUM_RESET_WORK_NAME;
98     private static final String ENDLESS_WORK_NAME;
99 
100     static {
101         final String syncPeriodicPrefix = "SYNC_MEDIA_PERIODIC_";
102         final String syncProactivePrefix = "SYNC_MEDIA_PROACTIVE_";
103         final String syncImmediatePrefix = "SYNC_MEDIA_IMMEDIATE_";
104         final String syncAllSuffix = "ALL";
105         final String syncLocalSuffix = "LOCAL";
106         final String syncCloudSuffix = "CLOUD";
107 
108         PERIODIC_ALBUM_RESET_WORK_NAME = "RESET_ALBUM_MEDIA_PERIODIC";
109         PERIODIC_SYNC_WORK_NAME = syncPeriodicPrefix + syncAllSuffix;
110         PROACTIVE_LOCAL_SYNC_WORK_NAME = syncProactivePrefix + syncLocalSuffix;
111         PROACTIVE_SYNC_WORK_NAME = syncProactivePrefix + syncAllSuffix;
112         IMMEDIATE_LOCAL_SYNC_WORK_NAME = syncImmediatePrefix + syncLocalSuffix;
113         IMMEDIATE_CLOUD_SYNC_WORK_NAME = syncImmediatePrefix + syncCloudSuffix;
114         IMMEDIATE_ALBUM_SYNC_WORK_NAME = "SYNC_ALBUM_MEDIA_IMMEDIATE";
115         ENDLESS_WORK_NAME = "ENDLESS_WORK";
116     }
117 
118     private final WorkManager mWorkManager;
119     private final ConfigStore mConfigStore;
120     private final Context mContext;
121 
PickerSyncManager(@onNull WorkManager workManager, @NonNull Context context, @NonNull ConfigStore configStore, boolean shouldSchedulePeriodicSyncs)122     public PickerSyncManager(@NonNull WorkManager workManager,
123             @NonNull Context context,
124             @NonNull ConfigStore configStore,
125             boolean shouldSchedulePeriodicSyncs) {
126         mWorkManager = requireNonNull(workManager);
127         mConfigStore = requireNonNull(configStore);
128         mContext = requireNonNull(context);
129 
130         setUpEndlessWork();
131 
132         if (shouldSchedulePeriodicSyncs) {
133             setUpPeriodicWork();
134         }
135 
136         // Subscribe to device config changes so we can enable periodic workers if Cloud
137         // Photopicker is enabled.
138         mConfigStore.addOnChangeListener(BackgroundThread.getExecutor(), this::setUpPeriodicWork);
139     }
140 
141     /**
142      * Will register new unique {@link Worker} for periodic sync and picker database maintenance if
143      * the cloud photopicker experiment is currently enabled.
144      */
setUpPeriodicWork()145     private void setUpPeriodicWork() {
146 
147         if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
148             PickerSyncNotificationHelper.createNotificationChannel(mContext);
149 
150             schedulePeriodicSyncs();
151             schedulePeriodicAlbumReset();
152         } else {
153             // Disable any scheduled ongoing work if the feature is disabled.
154             mWorkManager.cancelUniqueWork(PERIODIC_SYNC_WORK_NAME);
155             mWorkManager.cancelUniqueWork(PERIODIC_ALBUM_RESET_WORK_NAME);
156         }
157     }
158 
159     /**
160      * Will register a new {@link Worker} for 1 year in the future. This is to prevent the {@link
161      * androidx.work.impl.background.systemalarm.RescheduleReceiver} from being disabled by WM
162      * internals, which triggers PACKAGE_CHANGED broadcasts every time a new worker is scheduled. As
163      * a work around to prevent these broadcasts, we enqueue a worker here very far in the future to
164      * prevent the component from being disabled by work manager.
165      *
166      * <p>{@see b/314863434 for additional context.}
167      */
setUpEndlessWork()168     private void setUpEndlessWork() {
169 
170         OneTimeWorkRequest request =
171                 new OneTimeWorkRequest.Builder(EndlessWorker.class)
172                         .setInitialDelay(365, TimeUnit.DAYS)
173                         .build();
174 
175         mWorkManager.enqueueUniqueWork(
176                 ENDLESS_WORK_NAME, ExistingWorkPolicy.KEEP, request);
177         Log.d(TAG, "EndlessWorker has been enqueued");
178     }
179 
schedulePeriodicSyncs()180     private void schedulePeriodicSyncs() {
181         Log.i(TAG, "Scheduling periodic proactive syncs");
182 
183         final Data inputData =
184                 new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
185         final PeriodicWorkRequest periodicSyncRequest = getPeriodicProactiveSyncRequest(inputData);
186 
187         try {
188             // Note that the first execution of periodic work happens immediately or as soon as the
189             // given Constraints are met.
190             final Operation enqueueOperation = mWorkManager
191                     .enqueueUniquePeriodicWork(
192                             PERIODIC_SYNC_WORK_NAME,
193                             ExistingPeriodicWorkPolicy.KEEP,
194                             periodicSyncRequest
195                     );
196 
197             // Check that the request has been successfully enqueued.
198             enqueueOperation.getResult().get();
199         } catch (InterruptedException | ExecutionException e) {
200             Log.e(TAG, "Could not enqueue periodic proactive picker sync request", e);
201         }
202     }
203 
schedulePeriodicAlbumReset()204     private void schedulePeriodicAlbumReset() {
205         Log.i(TAG, "Scheduling periodic picker album data resets");
206 
207         final Data inputData =
208                 new Data(
209                         Map.of(
210                                 SYNC_WORKER_INPUT_SYNC_SOURCE,
211                                 SYNC_LOCAL_AND_CLOUD,
212                                 SYNC_WORKER_INPUT_RESET_TYPE,
213                                 SYNC_RESET_ALBUM));
214         final PeriodicWorkRequest periodicAlbumResetRequest =
215                 getPeriodicAlbumResetRequest(inputData);
216 
217         try {
218             // Note that the first execution of periodic work happens immediately or as soon
219             // as the given Constraints are met.
220             Operation enqueueOperation =
221                     mWorkManager.enqueueUniquePeriodicWork(
222                             PERIODIC_ALBUM_RESET_WORK_NAME,
223                             ExistingPeriodicWorkPolicy.KEEP,
224                             periodicAlbumResetRequest);
225 
226             // Check that the request has been successfully enqueued.
227             enqueueOperation.getResult().get();
228         } catch (InterruptedException | ExecutionException e) {
229             Log.e(TAG, "Could not enqueue periodic picker album resets request", e);
230         }
231     }
232 
233     /**
234      * Use this method for proactive syncs. The sync might take a while to start. Some device state
235      * conditions may apply before the sync can start like battery level etc.
236      *
237      * @param localOnly - whether the proactive sync should only sync with the local provider.
238      */
syncMediaProactively(Boolean localOnly)239     public void syncMediaProactively(Boolean localOnly) {
240 
241         final int syncSource = localOnly ? SYNC_LOCAL_ONLY : SYNC_LOCAL_AND_CLOUD;
242         final String workName =
243                 localOnly ? PROACTIVE_LOCAL_SYNC_WORK_NAME : PROACTIVE_SYNC_WORK_NAME;
244 
245         final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
246         final OneTimeWorkRequest syncRequest = getOneTimeProactiveSyncRequest(inputData);
247 
248         // Don't wait for the sync operation to enqueue so that Picker sync enqueue
249         // requests in
250         // order to avoid adding latency to critical MP code paths.
251 
252         mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, syncRequest);
253     }
254 
255     /**
256      * Use this method for reactive syncs which are user triggered.
257      *
258      * @param shouldSyncLocalOnlyData if true indicates that the sync should only be triggered with
259      *                                the local provider. Otherwise, sync will be triggered for both
260      *                                local and cloud provider.
261      */
syncMediaImmediately(boolean shouldSyncLocalOnlyData)262     public void syncMediaImmediately(boolean shouldSyncLocalOnlyData) {
263         syncMediaImmediately(PickerSyncManager.SYNC_LOCAL_ONLY, IMMEDIATE_LOCAL_SYNC_WORK_NAME);
264         if (!shouldSyncLocalOnlyData) {
265             syncMediaImmediately(PickerSyncManager.SYNC_CLOUD_ONLY, IMMEDIATE_CLOUD_SYNC_WORK_NAME);
266         }
267     }
268 
269     /**
270      * Use this method for reactive syncs with either, local and cloud providers, or both.
271      */
syncMediaImmediately(@yncSource int syncSource, @NonNull String workName)272     private void syncMediaImmediately(@SyncSource int syncSource, @NonNull String workName) {
273         final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
274         final OneTimeWorkRequest syncRequest =
275                 buildOneTimeWorkerRequest(ImmediateSyncWorker.class, inputData);
276 
277         // Track the new sync request(s)
278         trackNewSyncRequests(syncSource, syncRequest.getId());
279 
280         // Enqueue local or cloud sync request
281         try {
282             final Operation enqueueOperation = mWorkManager
283                     .enqueueUniqueWork(workName, ExistingWorkPolicy.APPEND_OR_REPLACE, syncRequest);
284 
285             // Check that the request has been successfully enqueued.
286             enqueueOperation.getResult().get();
287         } catch (Exception e) {
288             Log.e(TAG, "Could not enqueue expedited picker sync request", e);
289             markSyncAsComplete(syncSource, syncRequest.getId());
290         }
291     }
292 
293     /**
294      * Use this method for reactive syncs which are user action triggered.
295      *
296      * @param albumId is the id of the album that needs to be synced.
297      * @param authority The authority of the album media.
298      * @param isLocal is {@code true} iff the album authority is of the local provider.
299      */
syncAlbumMediaForProviderImmediately( @onNull String albumId, @NonNull String authority, boolean isLocal)300     public void syncAlbumMediaForProviderImmediately(
301             @NonNull String albumId, @NonNull String authority, boolean isLocal) {
302         syncAlbumMediaForProviderImmediately(albumId, getSyncSource(isLocal), authority);
303     }
304 
305     /**
306      * Use this method for reactive syncs which are user action triggered.
307      *
308      * @param albumId is the id of the album that needs to be synced.
309      * @param syncSource indicates if the sync is required with local provider or cloud provider or
310      *     both.
311      */
syncAlbumMediaForProviderImmediately( @onNull String albumId, @SyncSource int syncSource, String authority)312     private void syncAlbumMediaForProviderImmediately(
313             @NonNull String albumId, @SyncSource int syncSource, String authority) {
314         final Data inputData =
315                 new Data(
316                         Map.of(
317                                 SYNC_WORKER_INPUT_AUTHORITY, authority,
318                                 SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource,
319                                 SYNC_WORKER_INPUT_RESET_TYPE, SYNC_RESET_ALBUM,
320                                 SYNC_WORKER_INPUT_ALBUM_ID, albumId));
321         final OneTimeWorkRequest resetRequest =
322                 buildOneTimeWorkerRequest(MediaResetWorker.class, inputData);
323         final OneTimeWorkRequest syncRequest =
324                 buildOneTimeWorkerRequest(ImmediateAlbumSyncWorker.class, inputData);
325 
326         // Track the new sync request(s)
327         trackNewAlbumMediaSyncRequests(syncSource, resetRequest.getId());
328         trackNewAlbumMediaSyncRequests(syncSource, syncRequest.getId());
329 
330         // Enqueue local or cloud sync requests
331         try {
332             final Operation enqueueOperation =
333                     mWorkManager
334                             .beginUniqueWork(
335                                     IMMEDIATE_ALBUM_SYNC_WORK_NAME,
336                                     ExistingWorkPolicy.APPEND_OR_REPLACE,
337                                     resetRequest)
338                             .then(syncRequest).enqueue();
339 
340             // Check that the request has been successfully enqueued.
341             enqueueOperation.getResult().get();
342         } catch (Exception e) {
343             Log.e(TAG, "Could not enqueue expedited picker sync request", e);
344             markAlbumMediaSyncAsComplete(syncSource, resetRequest.getId());
345             markAlbumMediaSyncAsComplete(syncSource, syncRequest.getId());
346         }
347     }
348 
349     @NotNull
buildOneTimeWorkerRequest( @otNull Class<? extends Worker> workerClass, @NonNull Data inputData)350     private OneTimeWorkRequest buildOneTimeWorkerRequest(
351             @NotNull Class<? extends Worker> workerClass, @NonNull Data inputData) {
352         return new OneTimeWorkRequest.Builder(workerClass)
353                 .setInputData(inputData)
354                 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
355                 .build();
356     }
357 
358     @NotNull
getPeriodicProactiveSyncRequest(@otNull Data inputData)359     private PeriodicWorkRequest getPeriodicProactiveSyncRequest(@NotNull Data inputData) {
360         return new PeriodicWorkRequest.Builder(
361                 ProactiveSyncWorker.class, SYNC_MEDIA_PERIODIC_WORK_INTERVAL, TimeUnit.HOURS)
362                 .setInputData(inputData)
363                 .setConstraints(getRequiresChargingAndIdleConstraints())
364                 .build();
365     }
366 
367     @NotNull
getPeriodicAlbumResetRequest(@otNull Data inputData)368     private PeriodicWorkRequest getPeriodicAlbumResetRequest(@NotNull Data inputData) {
369 
370         return new PeriodicWorkRequest.Builder(
371                         MediaResetWorker.class,
372                         RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL,
373                         TimeUnit.HOURS)
374                 .setInputData(inputData)
375                 .setConstraints(getRequiresChargingAndIdleConstraints())
376                 .addTag(SYNC_WORKER_TAG_IS_PERIODIC)
377                 .build();
378     }
379 
380     @NotNull
getOneTimeProactiveSyncRequest(@otNull Data inputData)381     private OneTimeWorkRequest getOneTimeProactiveSyncRequest(@NotNull Data inputData) {
382         Constraints constraints =  new Constraints.Builder()
383                 .setRequiresBatteryNotLow(true)
384                 .build();
385 
386         return new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
387                 .setInputData(inputData)
388                 .setConstraints(constraints)
389                 .setInitialDelay(PROACTIVE_SYNC_DELAY_MS, TimeUnit.MILLISECONDS)
390                 .build();
391     }
392 
393     @NotNull
getRequiresChargingAndIdleConstraints()394     private static Constraints getRequiresChargingAndIdleConstraints() {
395         return new Constraints.Builder()
396                 .setRequiresCharging(true)
397                 .setRequiresDeviceIdle(true)
398                 .build();
399     }
400 
401     @SyncSource
getSyncSource(boolean isLocal)402     private static int getSyncSource(boolean isLocal) {
403         return isLocal
404                 ? SYNC_LOCAL_ONLY
405                 : SYNC_CLOUD_ONLY;
406     }
407 }
408