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