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