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 android.util.Log; 20 21 import androidx.annotation.VisibleForTesting; 22 23 import java.util.Collection; 24 import java.util.Collections; 25 import java.util.HashMap; 26 import java.util.Map; 27 import java.util.UUID; 28 import java.util.concurrent.CompletableFuture; 29 import java.util.concurrent.TimeUnit; 30 31 /** 32 * This class tracks all pending syncs in a synchronized map. 33 */ 34 public class SyncTracker { 35 private static final String TAG = "PickerSyncTracker"; 36 private static final long SYNC_FUTURE_TIMEOUT = 20; // Minutes 37 private static final Object FUTURE_RESULT = new Object(); // Placeholder result object 38 private final Map<UUID, CompletableFuture<Object>> mFutureMap = 39 Collections.synchronizedMap(new HashMap<>()); 40 41 /** 42 * Use this method to create a picker sync future and track its progress. This should be 43 * called either when a new sync request is enqueued, or when a new sync request starts 44 * processing. 45 * @param workRequestID the work request id of a picker sync. 46 */ createSyncFuture(UUID workRequestID)47 public void createSyncFuture(UUID workRequestID) { 48 createSyncFuture(workRequestID, SYNC_FUTURE_TIMEOUT, TimeUnit.MINUTES); 49 } 50 51 /** 52 * Use this method to create a picker sync future with a custom timeout. This method is 53 * intended to be used from tests. 54 */ 55 @VisibleForTesting(otherwise = VisibleForTesting.NONE) createSyncFuture(UUID workRequestID, long syncFutureTimeout, TimeUnit timeUnit)56 public void createSyncFuture(UUID workRequestID, long syncFutureTimeout, TimeUnit timeUnit) { 57 // Create a CompletableFuture that tracks a sync operation. The future will 58 // automatically be marked as finished after a given timeout. This is important because 59 // we're not able to track all WorkManager failures. In case of a failure to run the 60 // sync, we'll need to ensure that the future expires automatically after a given 61 // timeout. 62 final CompletableFuture<Object> syncFuture = new CompletableFuture<>(); 63 syncFuture.completeOnTimeout(FUTURE_RESULT, syncFutureTimeout, timeUnit); 64 mFutureMap.put(workRequestID, syncFuture); 65 Log.i(TAG, String.format("Created new sync future %s. Future map: %s", 66 syncFuture, mFutureMap)); 67 } 68 69 /** 70 * Use this method to mark a picker sync future as complete. If this is not invoked within a 71 * configured time limit, the future will automatically be set as done. 72 * @param workRequestID the work request id of a picker sync. 73 */ markSyncCompleted(UUID workRequestID)74 public void markSyncCompleted(UUID workRequestID) { 75 synchronized (mFutureMap) { 76 if (mFutureMap.containsKey(workRequestID)) { 77 mFutureMap.get(workRequestID).complete(FUTURE_RESULT); 78 mFutureMap.remove(workRequestID); 79 Log.i(TAG, String.format( 80 "Marked sync future complete for work id: %s. Future map: %s", 81 workRequestID, mFutureMap)); 82 } else { 83 Log.w(TAG, String.format("Attempted to complete sync future that is not currently " 84 + "tracked for work id: %s. Future map: %s", 85 workRequestID, mFutureMap)); 86 } 87 } 88 } 89 90 /** 91 * Use this method to mark all picker sync futures as complete. 92 * 93 * This is useful when using {@link ExistingWorkPolicy.REPLACE} to enqueue Unique Work. 94 * It clears the tracker of the work that will get cancelled by the REPLACE policy. 95 */ markAllSyncsCompleted()96 public void markAllSyncsCompleted() { 97 synchronized (mFutureMap) { 98 for (CompletableFuture future : mFutureMap.values()) { 99 future.complete(FUTURE_RESULT); 100 } 101 mFutureMap.clear(); 102 Log.i(TAG, String.format("Marked all sync futures as complete")); 103 } 104 } 105 106 /** 107 * Use this method to check if any sync request is still pending. 108 * @return a {@link Collection} of {@link CompletableFuture} of pending syncs. This can be 109 * used to track when all pending are complete. 110 */ pendingSyncFutures()111 public Collection<CompletableFuture<Object>> pendingSyncFutures() { 112 flushAllCompleteFutures(); 113 Log.i(TAG, String.format("Returning pending sync future map: %s", mFutureMap)); 114 return mFutureMap.values(); 115 } 116 flushAllCompleteFutures()117 private void flushAllCompleteFutures() { 118 // The synchronized map only guarantees serial access if all access to the backing map 119 // is accomplished through the returned map. Since the removeIf() method uses iterators to 120 // access the underlying map, it should be in a synchronized block. 121 Log.d(TAG, String.format("Flushing all complete futures: %s", mFutureMap)); 122 synchronized (mFutureMap) { 123 mFutureMap.values().removeIf(CompletableFuture::isDone); 124 } 125 } 126 } 127