blob: 4e9030f8045c9d61363858d00ada419a13fbc142 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.condition;
import android.util.ArraySet;
import android.util.Log;
import com.android.systemui.dagger.qualifiers.Main;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import javax.inject.Inject;
/**
* {@link Monitor} takes in a set of conditions, monitors whether all of them have
* been fulfilled, and informs any registered listeners.
*/
public class Monitor {
private final String mTag = getClass().getSimpleName();
private final Executor mExecutor;
private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>();
private final HashMap<Subscription.Token, SubscriptionState> mSubscriptions = new HashMap<>();
private static class SubscriptionState {
private final Subscription mSubscription;
private Boolean mAllConditionsMet;
SubscriptionState(Subscription subscription) {
mSubscription = subscription;
}
public Set<Condition> getConditions() {
return mSubscription.mConditions;
}
public void update() {
// Overriding conditions do not override each other
final Collection<Condition> overridingConditions = mSubscription.mConditions.stream()
.filter(Condition::isOverridingCondition).collect(Collectors.toSet());
final Collection<Condition> targetCollection = overridingConditions.isEmpty()
? mSubscription.mConditions : overridingConditions;
final boolean newAllConditionsMet = targetCollection.isEmpty() ? true : targetCollection
.stream()
.map(Condition::isConditionMet)
.allMatch(conditionMet -> conditionMet);
if (mAllConditionsMet != null && newAllConditionsMet == mAllConditionsMet) {
return;
}
mAllConditionsMet = newAllConditionsMet;
mSubscription.mCallback.onConditionsChanged(mAllConditionsMet);
}
}
// Callback for when each condition has been updated.
private final Condition.Callback mConditionCallback = new Condition.Callback() {
@Override
public void onConditionChanged(Condition condition) {
mExecutor.execute(() -> updateConditionMetState(condition));
}
};
@Inject
public Monitor(@Main Executor executor) {
mExecutor = executor;
}
private void updateConditionMetState(Condition condition) {
mConditions.get(condition).stream().forEach(token -> mSubscriptions.get(token).update());
}
/**
* Registers a callback and the set of conditions to trigger it.
* @param subscription A {@link Subscription} detailing the desired conditions and callback.
* @return A {@link Subscription.Token} that can be used to remove the subscription.
*/
public Subscription.Token addSubscription(@NotNull Subscription subscription) {
final Subscription.Token token = new Subscription.Token();
final SubscriptionState state = new SubscriptionState(subscription);
mExecutor.execute(() -> {
mSubscriptions.put(token, state);
// Add and associate conditions.
subscription.getConditions().stream().forEach(condition -> {
if (!mConditions.containsKey(condition)) {
mConditions.put(condition, new ArraySet<>());
condition.addCallback(mConditionCallback);
}
mConditions.get(condition).add(token);
});
// Update subscription state.
state.update();
});
return token;
}
/**
* Removes a subscription from participating in future callbacks.
* @param token The {@link Subscription.Token} returned when the {@link Subscription} was
* originally added.
*/
public void removeSubscription(@NotNull Subscription.Token token) {
mExecutor.execute(() -> {
if (shouldLog()) Log.d(mTag, "removing callback");
if (!mSubscriptions.containsKey(token)) {
Log.e(mTag, "subscription not present:" + token);
return;
}
mSubscriptions.remove(token).getConditions().forEach(condition -> {
if (!mConditions.containsKey(condition)) {
Log.e(mTag, "condition not present:" + condition);
return;
}
final Set<Subscription.Token> conditionSubscriptions = mConditions.get(condition);
conditionSubscriptions.remove(token);
if (conditionSubscriptions.isEmpty()) {
condition.removeCallback(mConditionCallback);
mConditions.remove(condition);
}
});
});
}
private boolean shouldLog() {
return Log.isLoggable(mTag, Log.DEBUG);
}
/**
* A {@link Subscription} represents a set of conditions and a callback that is informed when
* these conditions change.
*/
public static class Subscription {
private final Set<Condition> mConditions;
private final Callback mCallback;
/** */
public Subscription(Set<Condition> conditions, Callback callback) {
this.mConditions = Collections.unmodifiableSet(conditions);
this.mCallback = callback;
}
public Set<Condition> getConditions() {
return mConditions;
}
public Callback getCallback() {
return mCallback;
}
/**
* A {@link Token} is an identifier that is associated with a {@link Subscription} which is
* registered with a {@link Monitor}.
*/
public static class Token {
}
/**
* {@link Builder} is a helper class for constructing a {@link Subscription}.
*/
public static class Builder {
private final Callback mCallback;
private final ArraySet<Condition> mConditions;
/**
* Default constructor specifying the {@link Callback} for the {@link Subscription}.
* @param callback
*/
public Builder(Callback callback) {
mCallback = callback;
mConditions = new ArraySet<>();
}
/**
* Adds a {@link Condition} to be associated with the {@link Subscription}.
* @param condition
* @return The updated {@link Builder}.
*/
public Builder addCondition(Condition condition) {
mConditions.add(condition);
return this;
}
/**
* Adds a set of {@link Condition} to be associated with the {@link Subscription}.
* @param condition
* @return The updated {@link Builder}.
*/
public Builder addConditions(Set<Condition> condition) {
mConditions.addAll(condition);
return this;
}
/**
* Builds the {@link Subscription}.
* @return The resulting {@link Subscription}.
*/
public Subscription build() {
return new Subscription(mConditions, mCallback);
}
}
}
/**
* Callback that receives updates of whether all conditions have been fulfilled.
*/
public interface Callback {
/**
* Returns the conditions associated with this callback.
*/
default ArrayList<Condition> getConditions() {
return new ArrayList<>();
}
/**
* Triggered when the fulfillment of all conditions have been met.
*
* @param allConditionsMet True if all conditions have been fulfilled. False if none or
* only partial conditions have been fulfilled.
*/
void onConditionsChanged(boolean allConditionsMet);
}
}