1 /*
2  * Copyright (C) 2024 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.NonNull;
22 import androidx.annotation.VisibleForTesting;
23 import androidx.work.WorkInfo;
24 import androidx.work.WorkManager;
25 
26 import com.google.common.util.concurrent.ListenableFuture;
27 
28 import java.util.List;
29 import java.util.concurrent.CompletableFuture;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
33 
34 public class SyncCompletionWaiter {
35     private static final String TAG = "SyncCompletionWaiter";
36 
37     /**
38      * Will try it's best to wait for the existing sync requests to complete. It may not wait for
39      * new sync requests received after this method starts running.
40      */
waitForSync( @onNull WorkManager workManager, @NonNull SyncTracker syncTracker, @NonNull String uniqueWorkName)41     public static void waitForSync(
42             @NonNull WorkManager workManager,
43             @NonNull SyncTracker syncTracker,
44             @NonNull String uniqueWorkName) {
45         try {
46             final CompletableFuture<Void> completableFuture =
47                     CompletableFuture.allOf(
48                             syncTracker.pendingSyncFutures().toArray(new CompletableFuture[0]));
49 
50             waitForSync(workManager, completableFuture, uniqueWorkName, /* retryCount */ 30);
51         } catch (ExecutionException | InterruptedException e) {
52             Log.w(TAG, "Could not wait for the sync to finish: " + e);
53         }
54     }
55 
56     /**
57      * Wait for sync tracked by the input future to complete. In case the future takes an unusually
58      * long time to complete, check the relevant unique work status from Work Manager.
59      */
60     @VisibleForTesting
waitForSync( @onNull WorkManager workManager, @NonNull CompletableFuture<Void> completableFuture, @NonNull String uniqueWorkName, int retryCount)61     public static int waitForSync(
62             @NonNull WorkManager workManager,
63             @NonNull CompletableFuture<Void> completableFuture,
64             @NonNull String uniqueWorkName,
65             int retryCount) throws ExecutionException, InterruptedException {
66         for (; retryCount > 0; retryCount--) {
67             try {
68                 completableFuture.get(/* timeout */ 3, TimeUnit.SECONDS);
69                 return retryCount;
70             } catch (TimeoutException e) {
71                 if (isUniqueWorkPending(workManager, uniqueWorkName)) {
72                     Log.i(TAG, "Waiting for the sync again."
73                             + " Unique work name: " + uniqueWorkName
74                             + " Retry count: " + retryCount);
75                 } else {
76                     Log.e(TAG, "Either immediate unique work is complete and the sync futures "
77                             + "were not cleared, or a proactive sync might be blocking the query. "
78                             + "Unblocking the query now for " + uniqueWorkName);
79                     return retryCount;
80                 }
81             }
82         }
83 
84         if (retryCount == 0) {
85             Log.e(TAG, "Retry count exhausted, could not wait for sync anymore.");
86         }
87         return retryCount;
88     }
89 
90     /**
91      * Will wait for the existing sync requests to complete till the provided timeout. It may
92      * not wait for new sync requests received after this method starts running.
93      */
waitForSyncWithTimeout( @onNull SyncTracker syncTracker, int timeoutInMillis)94     public static boolean waitForSyncWithTimeout(
95             @NonNull SyncTracker syncTracker,
96             int timeoutInMillis) {
97         try {
98             final CompletableFuture<Void> completableFuture =
99                     CompletableFuture.allOf(
100                             syncTracker.pendingSyncFutures().toArray(new CompletableFuture[0]));
101             completableFuture.get(timeoutInMillis, TimeUnit.MILLISECONDS);
102             return true;
103         } catch (ExecutionException | InterruptedException | TimeoutException e) {
104             Log.w(TAG, "Could not wait for the sync with timeout to finish: " + e);
105             return false;
106         }
107     }
108 
109     /**
110      * Returns true if the given unique work is pending. In case the unique work is complete or
111      * there was an error in getting the work state, it returns false.
112      */
isUniqueWorkPending(WorkManager workManager, String uniqueWorkName)113     public static boolean isUniqueWorkPending(WorkManager workManager, String uniqueWorkName) {
114         ListenableFuture<List<WorkInfo>> future =
115                 workManager.getWorkInfosForUniqueWork(uniqueWorkName);
116         try {
117             List<WorkInfo> workInfos = future.get();
118             for (WorkInfo workInfo : workInfos) {
119                 if (!workInfo.getState().isFinished()) {
120                     return true;
121                 }
122             }
123             return false;
124         } catch (InterruptedException | ExecutionException e) {
125             Log.e(TAG, "Error occurred in fetching work info - ignore pending work");
126             return false;
127         }
128     }
129 }
130