• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 Google LLC
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  *   https://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 package com.google.android.enterprise.connectedapps.internal;
17 
18 import com.google.android.enterprise.connectedapps.Profile;
19 import com.google.errorprone.annotations.concurrent.GuardedBy;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Set;
24 
25 /** Receives a number of async results, merge them, and relays the merged results. */
26 public class CrossProfileCallbackMultiMerger<R> {
27 
28   // TODO: This should time-out if it doesn't receive the expected number
29 
30   /**
31    * A listener for results from the {@link CrossProfileCallbackMultiMerger}.
32    *
33    * <p>This will be called when all results are received.
34    */
35   public interface CrossProfileCallbackMultiMergerCompleteListener<R> {
onResult(Map<Profile, R> results)36     void onResult(Map<Profile, R> results);
37   }
38 
39   private final Object lock = new Object();
40   private final int expectedResults;
41 
42   @GuardedBy("lock")
43   private boolean hasCompleted = false;
44 
45   @GuardedBy("lock")
46   private final Map<Profile, R> results = new HashMap<>();
47 
48   @GuardedBy("lock")
49   private final Set<Profile> missedResults = new HashSet<>();
50 
51   private final CrossProfileCallbackMultiMergerCompleteListener<R> listener;
52 
CrossProfileCallbackMultiMerger( int expectedResults, CrossProfileCallbackMultiMergerCompleteListener<R> listener)53   public CrossProfileCallbackMultiMerger(
54       int expectedResults, CrossProfileCallbackMultiMergerCompleteListener<R> listener) {
55     if (listener == null) {
56       throw new NullPointerException();
57     }
58 
59     this.expectedResults = expectedResults;
60     this.listener = listener;
61 
62     checkIfCompleted();
63   }
64 
65   /**
66    * Indicate that a result is missing, so results can be posted with fewer than expected.
67    *
68    * <p>This should be called for every missing result. For example, if a remote call fails.
69    */
missingResult(Profile profileId)70   public void missingResult(Profile profileId) {
71     synchronized (lock) {
72       if (hasCompleted) {
73         // Once a result has been posted we don't check any more
74         return;
75       }
76 
77       if (results.containsKey(profileId) || missedResults.contains(profileId)) {
78         // Only one result per profile is accepted
79         return;
80       }
81       missedResults.add(profileId);
82     }
83 
84     checkIfCompleted();
85   }
86 
onResult(Profile profileId, R value)87   public void onResult(Profile profileId, R value) {
88     synchronized (lock) {
89       if (hasCompleted) {
90         // Once a result has been posted we don't check any more
91         return;
92       }
93       if (results.containsKey(profileId) || missedResults.contains(profileId)) {
94         // Only one result per profile is accepted
95         return;
96       }
97 
98       results.put(profileId, value);
99     }
100 
101     checkIfCompleted();
102   }
103 
checkIfCompleted()104   private void checkIfCompleted() {
105     Map<Profile, R> resultsCopy = null;
106     synchronized (lock) {
107       if (results.size() + missedResults.size() >= expectedResults) {
108         hasCompleted = true;
109         // Some tests rely on values in the map potentially being null, so using HashMap instead of
110         // ImmutableMap here as some production code may depend on this behavior.
111         resultsCopy = new HashMap<>(results);
112       }
113     }
114     // Listener notified outside the lock to avoid potential deadlocks.
115     if (resultsCopy != null) {
116       listener.onResult(resultsCopy);
117     }
118   }
119 }
120