blob: 1f61c64dd057a276ced5d06ba3ed6c90e2073e46 [file] [log] [blame]
/*
* Copyright (C) 2022 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.shared.condition;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.log.TableLogBufferBase;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* {@link Monitor} allows {@link Subscription}s to a set of conditions and monitors whether all of
* them have been fulfilled.
* <p>
* This class should be used as a singleton, to prevent duplicate monitoring of the same conditions.
*/
public class Monitor {
private final String mTag = getClass().getSimpleName();
private final Executor mExecutor;
private final Set<Condition> mPreconditions;
private final TableLogBufferBase mLogBuffer;
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;
// A subscription must maintain a reference to any active nested subscription so that it may
// be later removed when the current subscription becomes invalid.
private Subscription.Token mNestedSubscriptionToken;
private Boolean mAllConditionsMet;
private boolean mActive;
SubscriptionState(Subscription subscription) {
mSubscription = subscription;
}
public Set<Condition> getConditions() {
return mSubscription.mConditions;
}
/**
* Signals that the {@link Subscription} is now being monitored and will receive updates
* based on its conditions.
*/
private void setActive(boolean active) {
if (mActive == active) {
return;
}
mActive = active;
final Callback callback = mSubscription.getCallback();
if (callback == null) {
return;
}
callback.onActiveChanged(active);
}
public void update(Monitor monitor) {
final Boolean result = Evaluator.INSTANCE.evaluate(mSubscription.mConditions,
Evaluator.OP_AND);
// Consider unknown (null) as true
final boolean newAllConditionsMet = result == null || result;
if (mAllConditionsMet != null && newAllConditionsMet == mAllConditionsMet) {
return;
}
mAllConditionsMet = newAllConditionsMet;
final Subscription nestedSubscription = mSubscription.getNestedSubscription();
if (nestedSubscription != null) {
if (mAllConditionsMet && mNestedSubscriptionToken == null) {
// When all conditions are met for a subscription with a nested subscription
// that is not currently being monitored, add the nested subscription for
// monitor.
mNestedSubscriptionToken =
monitor.addSubscription(nestedSubscription, null);
} else if (!mAllConditionsMet && mNestedSubscriptionToken != null) {
// When conditions are not met and there is an active nested condition, remove
// the nested condition from monitoring.
removeNestedSubscription(monitor);
}
return;
}
mSubscription.getCallback().onConditionsChanged(mAllConditionsMet);
}
/**
* Invoked when the {@link Subscription} has been added to the {@link Monitor}.
*/
public void onAdded() {
setActive(true);
}
/**
* Invoked when the {@link Subscription} has been removed from the {@link Monitor},
* allowing cleanup code to run.
*/
public void onRemoved(Monitor monitor) {
setActive(false);
removeNestedSubscription(monitor);
}
private void removeNestedSubscription(Monitor monitor) {
if (mNestedSubscriptionToken == null) {
return;
}
monitor.removeSubscription(mNestedSubscriptionToken);
mNestedSubscriptionToken = null;
}
}
// 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));
}
};
/**
* Constructor for injected use-cases. By default, no preconditions are present.
*/
@Inject
public Monitor(@Main Executor executor) {
this(executor, Collections.emptySet());
}
/**
* Main constructor, allowing specifying preconditions.
*/
public Monitor(Executor executor, Set<Condition> preconditions) {
this(executor, preconditions, null);
}
/**
* Main constructor, allowing specifying preconditions and a log buffer for logging.
*/
public Monitor(Executor executor, Set<Condition> preconditions, TableLogBufferBase logBuffer) {
mExecutor = executor;
mPreconditions = preconditions;
mLogBuffer = logBuffer;
}
private void updateConditionMetState(Condition condition) {
if (mLogBuffer != null) {
mLogBuffer.logChange(/* prefix= */ "", condition.getTag(), condition.getState());
}
final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition);
// It's possible the condition was removed between the time the callback occurred and
// update was executed on the main thread.
if (subscriptions == null) {
return;
}
subscriptions.stream().forEach(token -> mSubscriptions.get(token).update(this));
}
/**
* 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(@NonNull Subscription subscription) {
return addSubscription(subscription, mPreconditions);
}
private Subscription.Token addSubscription(@NonNull Subscription subscription,
Set<Condition> preconditions) {
// If preconditions are set on the monitor, set up as a nested condition.
final Subscription normalizedCondition = preconditions != null
? new Subscription.Builder(subscription).addConditions(preconditions).build()
: subscription;
final Subscription.Token token = new Subscription.Token();
final SubscriptionState state = new SubscriptionState(normalizedCondition);
mExecutor.execute(() -> {
if (shouldLog()) Log.d(mTag, "adding subscription");
mSubscriptions.put(token, state);
// Add and associate conditions.
normalizedCondition.getConditions().stream().forEach(condition -> {
if (!mConditions.containsKey(condition)) {
mConditions.put(condition, new ArraySet<>());
condition.addCallback(mConditionCallback);
}
mConditions.get(condition).add(token);
});
state.onAdded();
// Update subscription state.
state.update(this);
});
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(@NonNull Subscription.Token token) {
mExecutor.execute(() -> {
if (shouldLog()) Log.d(mTag, "removing subscription");
if (!mSubscriptions.containsKey(token)) {
Log.e(mTag, "subscription not present:" + token);
return;
}
final SubscriptionState removedSubscription = mSubscriptions.remove(token);
removedSubscription.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);
}
});
removedSubscription.onRemoved(this);
});
}
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;
// A nested {@link Subscription} is a special callback where the specified condition's
// active state is dependent on the conditions of the parent {@link Subscription} being met.
// Once active, the nested subscription's conditions are registered as normal with the
// monitor and its callback (which could also be a nested condition) is triggered based on
// those conditions. The nested condition will be removed from monitor if the outer
// subscription's conditions ever become invalid.
private final Subscription mNestedSubscription;
private Subscription(Set<Condition> conditions, Callback callback,
Subscription nestedSubscription) {
this.mConditions = Collections.unmodifiableSet(conditions);
this.mCallback = callback;
this.mNestedSubscription = nestedSubscription;
}
public Set<Condition> getConditions() {
return mConditions;
}
public Callback getCallback() {
return mCallback;
}
public Subscription getNestedSubscription() {
return mNestedSubscription;
}
/**
* 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 Subscription mNestedSubscription;
private final ArraySet<Condition> mConditions;
private final ArraySet<Condition> mPreconditions;
/**
* Default constructor specifying the {@link Callback} for the {@link Subscription}.
*/
public Builder(Callback callback) {
this(null, callback);
}
public Builder(Subscription nestedSubscription) {
this(nestedSubscription, null);
}
private Builder(Subscription nestedSubscription, Callback callback) {
mNestedSubscription = nestedSubscription;
mCallback = callback;
mConditions = new ArraySet();
mPreconditions = new ArraySet();
}
/**
* Adds a {@link Condition} to be associated with the {@link Subscription}.
*
* @return The updated {@link Builder}.
*/
public Builder addCondition(Condition condition) {
mConditions.add(condition);
return this;
}
/**
* Adds a set of {@link Condition} to be a precondition for {@link Subscription}.
*
* @return The updated {@link Builder}.
*/
public Builder addPreconditions(Set<Condition> condition) {
if (condition == null) {
return this;
}
mPreconditions.addAll(condition);
return this;
}
/**
* Adds a {@link Condition} to be a precondition for {@link Subscription}.
*
* @return The updated {@link Builder}.
*/
public Builder addPrecondition(Condition condition) {
mPreconditions.add(condition);
return this;
}
/**
* Adds a set of {@link Condition} to be associated with the {@link Subscription}.
*
* @return The updated {@link Builder}.
*/
public Builder addConditions(Set<Condition> condition) {
if (condition == null) {
return this;
}
mConditions.addAll(condition);
return this;
}
/**
* Builds the {@link Subscription}.
*
* @return The resulting {@link Subscription}.
*/
public Subscription build() {
final Subscription subscription =
new Subscription(mConditions, mCallback, mNestedSubscription);
return !mPreconditions.isEmpty()
? new Subscription(mPreconditions, null, subscription)
: subscription;
}
}
}
/**
* 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);
/**
* Called when the active state of the {@link Subscription} changes.
* @param active {@code true} when changes to the conditions will affect the
* {@link Subscription}, {@code false} otherwise.
*/
default void onActiveChanged(boolean active) {
}
}
}