/*
* Copyright 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 androidx.wear.protolayout.expression.pipeline;
import android.icu.util.ULocale;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.wear.protolayout.expression.DynamicBuilders;
import androidx.wear.protolayout.expression.pipeline.BoolNodes.ComparisonFloatNode;
import androidx.wear.protolayout.expression.pipeline.BoolNodes.ComparisonInt32Node;
import androidx.wear.protolayout.expression.pipeline.BoolNodes.FixedBoolNode;
import androidx.wear.protolayout.expression.pipeline.BoolNodes.LogicalBoolOp;
import androidx.wear.protolayout.expression.pipeline.BoolNodes.NotBoolOp;
import androidx.wear.protolayout.expression.pipeline.BoolNodes.StateBoolNode;
import androidx.wear.protolayout.expression.pipeline.ColorNodes.AnimatableFixedColorNode;
import androidx.wear.protolayout.expression.pipeline.ColorNodes.DynamicAnimatedColorNode;
import androidx.wear.protolayout.expression.pipeline.ColorNodes.FixedColorNode;
import androidx.wear.protolayout.expression.pipeline.ColorNodes.StateColorSourceNode;
import androidx.wear.protolayout.expression.pipeline.DurationNodes.BetweenInstancesNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.AnimatableFixedFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.ArithmeticFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.DynamicAnimatedFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.FixedFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.Int32ToFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatSourceNode;
import androidx.wear.protolayout.expression.pipeline.InstantNodes.FixedInstantNode;
import androidx.wear.protolayout.expression.pipeline.InstantNodes.PlatformTimeSourceNode;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.AnimatableFixedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.ArithmeticInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.DynamicAnimatedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FixedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FloatToInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.GetDurationPartOpNode;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.PlatformInt32SourceNode;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.StateInt32SourceNode;
import androidx.wear.protolayout.expression.pipeline.StringNodes.FixedStringNode;
import androidx.wear.protolayout.expression.pipeline.StringNodes.FloatFormatNode;
import androidx.wear.protolayout.expression.pipeline.StringNodes.Int32FormatNode;
import androidx.wear.protolayout.expression.pipeline.StringNodes.StateStringNode;
import androidx.wear.protolayout.expression.pipeline.StringNodes.StringConcatOpNode;
import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicColor;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalFloatOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalInt32Op;
import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalStringOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicBool;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicColor;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicDuration;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicFloat;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInstant;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
/**
* Evaluates protolayout dynamic types.
*
* <p>Given a dynamic ProtoLayout data source, this builds up a sequence of {@link DynamicDataNode}
* instances, which can source the required data, and transform it into its final form.
*
* <p>Data source can include animations which will then emit value transitions.
*
* <p>In order to evaluate dynamic types, the caller needs to add any number of pending dynamic
* types with {@link #bind} methods and then call {@link BoundDynamicType#startEvaluation()} on each
* of them to start evaluation. Starting evaluation can be done for batches of dynamic types.
*
* <p>It's the callers responsibility to destroy those dynamic types after use, with {@link
* BoundDynamicType#close()}.
*
* <p>It's the callers responsibility to destroy those dynamic types after use, with {@link
* BoundDynamicType#close()}.
*/
public class DynamicTypeEvaluator implements AutoCloseable {
private static final String TAG = "DynamicTypeEvaluator";
@Nullable private final SensorGateway mSensorGateway;
@Nullable private final SensorGatewayPlatformDataSource mSensorGatewayDataSource;
@NonNull private final TimeGatewayImpl mTimeGateway;
@Nullable private final EpochTimePlatformDataSource mTimeDataSource;
@NonNull private final ObservableStateStore mStateStore;
private final boolean mEnableAnimations;
@NonNull private final QuotaManager mAnimationQuotaManager;
@NonNull
private static final QuotaManager DISABLED_ANIMATIONS_QUOTA_MANAGER =
new QuotaManager() {
@Override
public boolean tryAcquireQuota(int quota) {
return false;
}
@Override
public void releaseQuota(int quota) {
throw new IllegalStateException(
"releaseQuota method is called when no quota is acquired!");
}
};
/**
* Creates a {@link DynamicTypeEvaluator} without animation support.
*
* @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
* sources should be allowed initially. After that, enabling updates from sensor and time
* sources can be done via {@link #enablePlatformDataSources()} or {@link
* #disablePlatformDataSources()}.
* @param sensorGateway The gateway for sensor data.
* @param stateStore The state store that will be used for dereferencing the state keys in the
* dynamic types.
*/
public DynamicTypeEvaluator(
boolean platformDataSourcesInitiallyEnabled,
@Nullable SensorGateway sensorGateway,
@NonNull ObservableStateStore stateStore) {
// Build pipeline with quota that doesn't allow any animations.
this(
platformDataSourcesInitiallyEnabled,
sensorGateway,
stateStore,
/* enableAnimations= */ false,
DISABLED_ANIMATIONS_QUOTA_MANAGER);
}
/**
* Creates a {@link DynamicTypeEvaluator} with animation support. Maximum number of concurrently
* running animations is defined in the given {@link QuotaManager}. Passing in animatable data
* source to any of the methods will emit value transitions, for example animatable float from 5
* to 10 will emit all values between those numbers (i.e. 5, 6, 7, 8, 9, 10).
*
* @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
* sources should be allowed initially. After that, enabling updates from sensor and time
* sources can be done via {@link #enablePlatformDataSources()} or {@link
* #disablePlatformDataSources()}.
* @param sensorGateway The gateway for sensor data.
* @param stateStore The state store that will be used for dereferencing the state keys in the
* dynamic types.
* @param animationQuotaManager The quota manager used for limiting the number of concurrently
* running animations.
*/
public DynamicTypeEvaluator(
boolean platformDataSourcesInitiallyEnabled,
@Nullable SensorGateway sensorGateway,
@NonNull ObservableStateStore stateStore,
@NonNull QuotaManager animationQuotaManager) {
this(
platformDataSourcesInitiallyEnabled,
sensorGateway,
stateStore,
/* enableAnimations= */ true,
animationQuotaManager);
}
/**
* Creates a {@link DynamicTypeEvaluator}.
*
* @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
* sources should be allowed initially. After that, enabling updates from sensor and time
* sources can be done via {@link #enablePlatformDataSources()} or {@link
* #disablePlatformDataSources()}.
* @param sensorGateway The gateway for sensor data.
* @param stateStore The state store that will be used for dereferencing the state keys in the
* dynamic types.
* @param animationQuotaManager The quota manager used for limiting the number of concurrently
* running animations.
*/
private DynamicTypeEvaluator(
boolean platformDataSourcesInitiallyEnabled,
@Nullable SensorGateway sensorGateway,
@NonNull ObservableStateStore stateStore,
boolean enableAnimations,
@NonNull QuotaManager animationQuotaManager) {
this.mSensorGateway = sensorGateway;
Handler uiHandler = new Handler(Looper.getMainLooper());
MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
if (this.mSensorGateway != null) {
if (platformDataSourcesInitiallyEnabled) {
this.mSensorGateway.enableUpdates();
} else {
this.mSensorGateway.disableUpdates();
}
this.mSensorGatewayDataSource =
new SensorGatewayPlatformDataSource(uiExecutor, this.mSensorGateway);
} else {
this.mSensorGatewayDataSource = null;
}
this.mTimeGateway = new TimeGatewayImpl(uiHandler, platformDataSourcesInitiallyEnabled);
this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, mTimeGateway);
this.mEnableAnimations = enableAnimations;
this.mStateStore = stateStore;
this.mAnimationQuotaManager = animationQuotaManager;
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicString} for evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param stringSource The given String dynamic type that should be evaluated.
* @param locale The locale used for the given String source.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicString stringSource,
@NonNull ULocale locale,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<String> consumer) {
return bind(
stringSource.toDynamicStringProto(),
locale,
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicString} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param stringSource The given String dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
* @param locale The locale used for the given String source.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicString stringSource,
@NonNull ULocale locale,
@NonNull DynamicTypeValueReceiver<String> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(stringSource, consumer, locale, resultBuilder);
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicInt32} for evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param int32Source The given integer dynamic type that should be evaluated.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicInt32 int32Source,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<Integer> consumer) {
return bind(
int32Source.toDynamicInt32Proto(),
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicInt32} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param int32Source The given integer dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicInt32 int32Source,
@NonNull DynamicTypeValueReceiver<Integer> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(int32Source, consumer, resultBuilder, Optional.empty());
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds pending expression from the given {@link DynamicInt32} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
* by caller, results of evaluation will be sent through the given {@link
* DynamicTypeValueReceiver}.
*
* @param int32Source The given integer dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
* @param animationFallbackValue The value used if the given {@link DynamicInt32} is animatable
* and animations are disabled.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicInt32 int32Source,
@NonNull DynamicTypeValueReceiver<Integer> consumer,
int animationFallbackValue) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(int32Source, consumer, resultBuilder, Optional.of(animationFallbackValue));
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicFloat} for evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param floatSource The given float dynamic type that should be evaluated.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicFloat floatSource,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<Float> consumer) {
return bind(
floatSource.toDynamicFloatProto(),
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicFloat} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param floatSource The given float dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
* @param animationFallbackValue The value used if the given {@link DynamicFloat} is animatable
* and animation are disabled.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicFloat floatSource,
@NonNull DynamicTypeValueReceiver<Float> consumer,
float animationFallbackValue) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(floatSource, consumer, resultBuilder, Optional.of(animationFallbackValue));
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds pending dynamic type from the given {@link DynamicFloat} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param floatSource The given float dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicFloat floatSource, @NonNull DynamicTypeValueReceiver<Float> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(floatSource, consumer, resultBuilder, Optional.empty());
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicColor} for evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param colorSource The given color dynamic type that should be evaluated.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicColor colorSource,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<Integer> consumer) {
return bind(
colorSource.toDynamicColorProto(),
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicColor} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param colorSource The given color dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicColor colorSource,
@NonNull DynamicTypeValueReceiver<Integer> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(colorSource, consumer, resultBuilder, Optional.empty());
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds pending dynamic type from the given {@link DynamicColor} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param colorSource The given color dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
* @param animationFallbackValue The value used if the given {@link DynamicFloat} is animatable
* and animation are disabled.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicColor colorSource,
@NonNull DynamicTypeValueReceiver<Integer> consumer,
int animationFallbackValue) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(colorSource, consumer, resultBuilder, Optional.of(animationFallbackValue));
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicDuration} for evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param durationSource The given duration dynamic type that should be evaluated.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicDuration durationSource,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<Duration> consumer) {
return bind(
durationSource.toDynamicDurationProto(),
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicDuration} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param durationSource The given durations dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicDuration durationSource,
@NonNull DynamicTypeValueReceiver<Duration> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(durationSource, consumer, resultBuilder);
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicInstant} for evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param instantSource The given instant dynamic type that should be evaluated.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicInstant instantSource,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<Instant> consumer) {
return bind(
instantSource.toDynamicInstantProto(),
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicInstant} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param instantSource The given instant dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicInstant instantSource,
@NonNull DynamicTypeValueReceiver<Instant> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(instantSource, consumer, resultBuilder);
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Adds dynamic type from the given {@link DynamicBuilders.DynamicBool} for evaluation.
* Evaluation will start immediately.
*
* <p>Results of evaluation will be sent through the given {@link DynamicTypeValueReceiver}
* on the given {@link Executor}.
*
* @param boolSource The given boolean dynamic type that should be evaluated.
* @param executor The Executor to run the consumer on.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
public BoundDynamicType bind(
@NonNull DynamicBuilders.DynamicBool boolSource,
@NonNull Executor executor,
@NonNull DynamicTypeValueReceiver<Boolean> consumer) {
return bind(
boolSource.toDynamicBoolProto(),
new DynamicTypeValueReceiverOnExecutor<>(executor, consumer));
}
/**
* Adds pending dynamic type from the given {@link DynamicBool} for future evaluation.
*
* <p>Evaluation of this dynamic type will start when {@link BoundDynamicType#startEvaluation()}
* is called on the returned object.
*
* @param boolSource The given boolean dynamic type that should be evaluated.
* @param consumer The registered consumer for results of the evaluation. It will be called from
* UI thread.
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public BoundDynamicType bind(
@NonNull DynamicBool boolSource, @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
bindRecursively(boolSource, consumer, resultBuilder);
return new BoundDynamicTypeImpl(resultBuilder);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicString stringSource,
@NonNull DynamicTypeValueReceiver<String> consumer,
@NonNull ULocale locale,
@NonNull List<DynamicDataNode<?>> resultBuilder) {
DynamicDataNode<?> node;
switch (stringSource.getInnerCase()) {
case FIXED:
node = new FixedStringNode(stringSource.getFixed(), consumer);
break;
case INT32_FORMAT_OP:
{
NumberFormatter formatter =
new NumberFormatter(stringSource.getInt32FormatOp(), locale);
Int32FormatNode int32FormatNode = new Int32FormatNode(formatter, consumer);
node = int32FormatNode;
bindRecursively(
stringSource.getInt32FormatOp().getInput(),
int32FormatNode.getIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case FLOAT_FORMAT_OP:
{
NumberFormatter formatter =
new NumberFormatter(stringSource.getFloatFormatOp(), locale);
FloatFormatNode floatFormatNode = new FloatFormatNode(formatter, consumer);
node = floatFormatNode;
bindRecursively(
stringSource.getFloatFormatOp().getInput(),
floatFormatNode.getIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case STATE_SOURCE:
{
node =
new StateStringNode(
mStateStore, stringSource.getStateSource(), consumer);
break;
}
case CONDITIONAL_OP:
{
ConditionalOpNode<String> conditionalNode = new ConditionalOpNode<>(consumer);
ConditionalStringOp op = stringSource.getConditionalOp();
bindRecursively(
op.getCondition(),
conditionalNode.getConditionIncomingCallback(),
resultBuilder);
bindRecursively(
op.getValueIfTrue(),
conditionalNode.getTrueValueIncomingCallback(),
locale,
resultBuilder);
bindRecursively(
op.getValueIfFalse(),
conditionalNode.getFalseValueIncomingCallback(),
locale,
resultBuilder);
node = conditionalNode;
break;
}
case CONCAT_OP:
{
StringConcatOpNode concatNode = new StringConcatOpNode(consumer);
node = concatNode;
bindRecursively(
stringSource.getConcatOp().getInputLhs(),
concatNode.getLhsIncomingCallback(),
locale,
resultBuilder);
bindRecursively(
stringSource.getConcatOp().getInputRhs(),
concatNode.getRhsIncomingCallback(),
locale,
resultBuilder);
break;
}
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicString has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicString source type");
}
resultBuilder.add(node);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicInt32 int32Source,
@NonNull DynamicTypeValueReceiver<Integer> consumer,
@NonNull List<DynamicDataNode<?>> resultBuilder,
@NonNull Optional<Integer> animationFallbackValue) {
DynamicDataNode<Integer> node;
switch (int32Source.getInnerCase()) {
case FIXED:
node = new FixedInt32Node(int32Source.getFixed(), consumer);
break;
case PLATFORM_SOURCE:
node =
new PlatformInt32SourceNode(
int32Source.getPlatformSource(),
mSensorGatewayDataSource,
consumer);
break;
case ARITHMETIC_OPERATION:
{
ArithmeticInt32Node arithmeticNode =
new ArithmeticInt32Node(int32Source.getArithmeticOperation(), consumer);
node = arithmeticNode;
bindRecursively(
int32Source.getArithmeticOperation().getInputLhs(),
arithmeticNode.getLhsIncomingCallback(),
resultBuilder,
Optional.empty());
bindRecursively(
int32Source.getArithmeticOperation().getInputRhs(),
arithmeticNode.getRhsIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case STATE_SOURCE:
{
node =
new StateInt32SourceNode(
mStateStore, int32Source.getStateSource(), consumer);
break;
}
case CONDITIONAL_OP:
{
ConditionalOpNode<Integer> conditionalNode = new ConditionalOpNode<>(consumer);
ConditionalInt32Op op = int32Source.getConditionalOp();
bindRecursively(
op.getCondition(),
conditionalNode.getConditionIncomingCallback(),
resultBuilder);
bindRecursively(
op.getValueIfTrue(),
conditionalNode.getTrueValueIncomingCallback(),
resultBuilder,
Optional.empty());
bindRecursively(
op.getValueIfFalse(),
conditionalNode.getFalseValueIncomingCallback(),
resultBuilder,
Optional.empty());
node = conditionalNode;
break;
}
case FLOAT_TO_INT:
{
FloatToInt32Node conversionNode =
new FloatToInt32Node(int32Source.getFloatToInt(), consumer);
node = conversionNode;
bindRecursively(
int32Source.getFloatToInt().getInput(),
conversionNode.getIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case DURATION_PART:
{
GetDurationPartOpNode durationPartOpNode =
new GetDurationPartOpNode(int32Source.getDurationPart(), consumer);
node = durationPartOpNode;
bindRecursively(
int32Source.getDurationPart().getInput(),
durationPartOpNode.getIncomingCallback(),
resultBuilder);
break;
}
case ANIMATABLE_FIXED:
if (!mEnableAnimations && animationFallbackValue.isPresent()) {
// Just assign static value if animations are disabled.
node =
new FixedInt32Node(
FixedInt32.newBuilder()
.setValue(animationFallbackValue.get())
.build(),
consumer);
} else {
// We don't have to check if enableAnimations is true, because if it's false and
// we didn't have static value set, constructor has put QuotaManager that don't
// have any quota, so animations won't be played and they would jump to the end
// value.
node =
new AnimatableFixedInt32Node(
int32Source.getAnimatableFixed(),
consumer,
mAnimationQuotaManager);
}
break;
case ANIMATABLE_DYNAMIC:
if (!mEnableAnimations && animationFallbackValue.isPresent()) {
// Just assign static value if animations are disabled.
node =
new FixedInt32Node(
FixedInt32.newBuilder()
.setValue(animationFallbackValue.get())
.build(),
consumer);
} else {
// We don't have to check if enableAnimations is true, because if it's false and
// we didn't have static value set, constructor has put QuotaManager that don't
// have any quota, so animations won't be played and they would jump to the end
// value.
AnimatableDynamicInt32 dynamicNode = int32Source.getAnimatableDynamic();
DynamicAnimatedInt32Node animationNode =
new DynamicAnimatedInt32Node(
consumer,
dynamicNode.getAnimationSpec(),
mAnimationQuotaManager);
node = animationNode;
bindRecursively(
dynamicNode.getInput(),
animationNode.getInputCallback(),
resultBuilder,
animationFallbackValue);
}
break;
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicInt32 has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicInt32 source type");
}
resultBuilder.add(node);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicDuration durationSource,
@NonNull DynamicTypeValueReceiver<Duration> consumer,
@NonNull List<DynamicDataNode<?>> resultBuilder) {
DynamicDataNode<?> node;
switch (durationSource.getInnerCase()) {
case BETWEEN:
BetweenInstancesNode betweenInstancesNode = new BetweenInstancesNode(consumer);
node = betweenInstancesNode;
bindRecursively(
durationSource.getBetween().getStartInclusive(),
betweenInstancesNode.getLhsIncomingCallback(),
resultBuilder);
bindRecursively(
durationSource.getBetween().getEndExclusive(),
betweenInstancesNode.getRhsIncomingCallback(),
resultBuilder);
break;
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicDuration has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicDuration source type");
}
resultBuilder.add(node);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicInstant instantSource,
@NonNull DynamicTypeValueReceiver<Instant> consumer,
@NonNull List<DynamicDataNode<?>> resultBuilder) {
DynamicDataNode<?> node;
switch (instantSource.getInnerCase()) {
case FIXED:
node = new FixedInstantNode(instantSource.getFixed(), consumer);
break;
case PLATFORM_SOURCE:
node = new PlatformTimeSourceNode(mTimeDataSource, consumer);
break;
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicInstant has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicInstant source type");
}
resultBuilder.add(node);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicFloat floatSource,
@NonNull DynamicTypeValueReceiver<Float> consumer,
@NonNull List<DynamicDataNode<?>> resultBuilder,
@NonNull Optional<Float> animationFallbackValue) {
DynamicDataNode<?> node;
switch (floatSource.getInnerCase()) {
case FIXED:
node = new FixedFloatNode(floatSource.getFixed(), consumer);
break;
case STATE_SOURCE:
node = new StateFloatSourceNode(
mStateStore, floatSource.getStateSource(), consumer);
break;
case ARITHMETIC_OPERATION:
{
ArithmeticFloatNode arithmeticNode =
new ArithmeticFloatNode(floatSource.getArithmeticOperation(), consumer);
node = arithmeticNode;
bindRecursively(
floatSource.getArithmeticOperation().getInputLhs(),
arithmeticNode.getLhsIncomingCallback(),
resultBuilder,
Optional.empty());
bindRecursively(
floatSource.getArithmeticOperation().getInputRhs(),
arithmeticNode.getRhsIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case INT32_TO_FLOAT_OPERATION:
{
Int32ToFloatNode toFloatNode = new Int32ToFloatNode(consumer);
node = toFloatNode;
bindRecursively(
floatSource.getInt32ToFloatOperation().getInput(),
toFloatNode.getIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case CONDITIONAL_OP:
{
ConditionalOpNode<Float> conditionalNode = new ConditionalOpNode<>(consumer);
ConditionalFloatOp op = floatSource.getConditionalOp();
bindRecursively(
op.getCondition(),
conditionalNode.getConditionIncomingCallback(),
resultBuilder);
bindRecursively(
op.getValueIfTrue(),
conditionalNode.getTrueValueIncomingCallback(),
resultBuilder,
Optional.empty());
bindRecursively(
op.getValueIfFalse(),
conditionalNode.getFalseValueIncomingCallback(),
resultBuilder,
Optional.empty());
node = conditionalNode;
break;
}
case ANIMATABLE_FIXED:
if (!mEnableAnimations && animationFallbackValue.isPresent()) {
// Just assign static value if animations are disabled.
node =
new FixedFloatNode(
FixedFloat.newBuilder()
.setValue(animationFallbackValue.get())
.build(),
consumer);
} else {
// We don't have to check if enableAnimations is true, because if it's false and
// we didn't have static value set, constructor has put QuotaManager that don't
// have any quota, so animations won't be played and they would jump to the end
// value.
node =
new AnimatableFixedFloatNode(
floatSource.getAnimatableFixed(),
consumer,
mAnimationQuotaManager);
}
break;
case ANIMATABLE_DYNAMIC:
if (!mEnableAnimations && animationFallbackValue.isPresent()) {
// Just assign static value if animations are disabled.
node =
new FixedFloatNode(
FixedFloat.newBuilder()
.setValue(animationFallbackValue.get())
.build(),
consumer);
} else {
// We don't have to check if enableAnimations is true, because if it's false and
// we didn't have static value set, constructor has put QuotaManager that don't
// have any quota, so animations won't be played and they would jump to the end
// value.
AnimatableDynamicFloat dynamicNode = floatSource.getAnimatableDynamic();
DynamicAnimatedFloatNode animationNode =
new DynamicAnimatedFloatNode(
consumer,
dynamicNode.getAnimationSpec(),
mAnimationQuotaManager
);
node = animationNode;
bindRecursively(
dynamicNode.getInput(),
animationNode.getInputCallback(),
resultBuilder,
animationFallbackValue);
}
break;
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicFloat has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicFloat source type");
}
resultBuilder.add(node);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicColor colorSource,
@NonNull DynamicTypeValueReceiver<Integer> consumer,
@NonNull List<DynamicDataNode<?>> resultBuilder,
@NonNull Optional<Integer> animationFallbackValue) {
DynamicDataNode<?> node;
switch (colorSource.getInnerCase()) {
case FIXED:
node = new FixedColorNode(colorSource.getFixed(), consumer);
break;
case STATE_SOURCE:
node =
new StateColorSourceNode(
mStateStore, colorSource.getStateSource(), consumer);
break;
case ANIMATABLE_FIXED:
if (!mEnableAnimations && animationFallbackValue.isPresent()) {
// Just assign static value if animations are disabled.
node =
new FixedColorNode(
FixedColor.newBuilder()
.setArgb(animationFallbackValue.get())
.build(),
consumer);
} else {
// We don't have to check if enableAnimations is true, because if it's false and
// we didn't have static value set, constructor has put QuotaManager that don't
// have any quota, so animations won't be played and they would jump to the end
// value.
node =
new AnimatableFixedColorNode(
colorSource.getAnimatableFixed(),
consumer,
mAnimationQuotaManager);
}
break;
case ANIMATABLE_DYNAMIC:
if (!mEnableAnimations && animationFallbackValue.isPresent()) {
// Just assign static value if animations are disabled.
node =
new FixedColorNode(
FixedColor.newBuilder()
.setArgb(animationFallbackValue.get())
.build(),
consumer);
} else {
// We don't have to check if enableAnimations is true, because if it's false and
// we didn't have static value set, constructor has put QuotaManager that don't
// have any quota, so animations won't be played and they would jump to the end
// value.
AnimatableDynamicColor dynamicNode = colorSource.getAnimatableDynamic();
DynamicAnimatedColorNode animationNode =
new DynamicAnimatedColorNode(
consumer,
dynamicNode.getAnimationSpec(),
mAnimationQuotaManager
);
node = animationNode;
bindRecursively(
dynamicNode.getInput(),
animationNode.getInputCallback(),
resultBuilder,
animationFallbackValue);
}
break;
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicColor has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicColor source type");
}
resultBuilder.add(node);
}
/**
* Same as {@link #bind}, but instead of returning one {@link BoundDynamicType}, all
* {@link DynamicDataNode} produced by evaluating given dynamic type are added to the given
* list.
*/
private void bindRecursively(
@NonNull DynamicBool boolSource,
@NonNull DynamicTypeValueReceiver<Boolean> consumer,
@NonNull List<DynamicDataNode<?>> resultBuilder) {
DynamicDataNode<?> node;
switch (boolSource.getInnerCase()) {
case FIXED:
node = new FixedBoolNode(boolSource.getFixed(), consumer);
break;
case STATE_SOURCE:
node = new StateBoolNode(mStateStore, boolSource.getStateSource(), consumer);
break;
case INT32_COMPARISON:
{
ComparisonInt32Node compNode =
new ComparisonInt32Node(boolSource.getInt32Comparison(), consumer);
node = compNode;
bindRecursively(
boolSource.getInt32Comparison().getInputLhs(),
compNode.getLhsIncomingCallback(),
resultBuilder,
Optional.empty());
bindRecursively(
boolSource.getInt32Comparison().getInputRhs(),
compNode.getRhsIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case LOGICAL_OP:
{
LogicalBoolOp logicalNode =
new LogicalBoolOp(boolSource.getLogicalOp(), consumer);
node = logicalNode;
bindRecursively(
boolSource.getLogicalOp().getInputLhs(),
logicalNode.getLhsIncomingCallback(),
resultBuilder);
bindRecursively(
boolSource.getLogicalOp().getInputRhs(),
logicalNode.getRhsIncomingCallback(),
resultBuilder);
break;
}
case NOT_OP:
{
NotBoolOp notNode = new NotBoolOp(consumer);
node = notNode;
bindRecursively(
boolSource.getNotOp().getInput(),
notNode.getIncomingCallback(),
resultBuilder);
break;
}
case FLOAT_COMPARISON:
{
ComparisonFloatNode compNode =
new ComparisonFloatNode(boolSource.getFloatComparison(), consumer);
node = compNode;
bindRecursively(
boolSource.getFloatComparison().getInputLhs(),
compNode.getLhsIncomingCallback(),
resultBuilder,
Optional.empty());
bindRecursively(
boolSource.getFloatComparison().getInputRhs(),
compNode.getRhsIncomingCallback(),
resultBuilder,
Optional.empty());
break;
}
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicBool has no inner source set");
default:
throw new IllegalArgumentException("Unknown DynamicBool source type");
}
resultBuilder.add(node);
}
/** Enables sending updates on sensor and time. */
@UiThread
public void enablePlatformDataSources() {
if (mSensorGateway != null) {
mSensorGateway.enableUpdates();
}
mTimeGateway.enableUpdates();
}
/** Disables sending updates on sensor and time. */
@UiThread
public void disablePlatformDataSources() {
if (mSensorGateway != null) {
mSensorGateway.disableUpdates();
}
mTimeGateway.disableUpdates();
}
/**
* Closes existing time gateway.
*
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void close() {
try {
mTimeGateway.close();
} catch (RuntimeException ex) {
Log.e(TAG, "Error while cleaning up time gateway", ex);
}
}
/**
* Wraps {@link DynamicTypeValueReceiver} and executes its methods on the given
* {@link Executor}.
*/
private static class DynamicTypeValueReceiverOnExecutor<T>
implements DynamicTypeValueReceiver<T> {
@NonNull private final Executor mExecutor;
@NonNull private final DynamicTypeValueReceiver<T> mConsumer;
DynamicTypeValueReceiverOnExecutor(
@NonNull Executor executor, @NonNull DynamicTypeValueReceiver<T> consumer) {
this.mConsumer = consumer;
this.mExecutor = executor;
}
@Override
@SuppressWarnings("ExecutorTaskName")
public void onPreUpdate() {
mExecutor.execute(mConsumer::onPreUpdate);
}
@Override
@SuppressWarnings("ExecutorTaskName")
public void onData(@NonNull T newData) {
mExecutor.execute(() -> mConsumer.onData(newData));
}
@Override
@SuppressWarnings("ExecutorTaskName")
public void onInvalidated() {
mExecutor.execute(mConsumer::onInvalidated);
}
}
}