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