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