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 check if any sync request is still pending. 92 * @return a {@link Collection} of {@link CompletableFuture} of pending syncs. This can be 93 * used to track when all pending are complete. 94 */ pendingSyncFutures()95 public Collection<CompletableFuture<Object>> pendingSyncFutures() { 96 flushAllCompleteFutures(); 97 Log.i(TAG, String.format("Returning pending sync future map: %s", mFutureMap)); 98 return mFutureMap.values(); 99 } 100 flushAllCompleteFutures()101 private void flushAllCompleteFutures() { 102 // The synchronized map only guarantees serial access if all access to the backing map 103 // is accomplished through the returned map. Since the removeIf() method uses iterators to 104 // access the underlying map, it should be in a synchronized block. 105 Log.d(TAG, String.format("Flushing all complete futures: %s", mFutureMap)); 106 synchronized (mFutureMap) { 107 mFutureMap.values().removeIf(CompletableFuture::isDone); 108 } 109 } 110 } 111