• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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