DynamicTypeEvaluator.java

/*
 * 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 static java.util.Collections.emptyMap;

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.ConditionalColorOp;
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 java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
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";

    @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!");
                }
            };

    @NonNull private static final StateStore EMPTY_STATE_STORE = new StateStore(emptyMap());

    @NonNull private final Config mConfig;
    @NonNull private final StateStore mStateStore;
    @NonNull private final QuotaManager mAnimationQuotaManager;
    @NonNull private final TimeGateway mTimeGateway;
    @Nullable private final EpochTimePlatformDataSource mTimeDataSource;
    @Nullable private final SensorGatewayPlatformDataSource mSensorGatewayDataSource;

    /** Configuration for {@link #DynamicTypeEvaluator(Config)}. */
    public static final class Config {
        private final boolean mPlatformDataSourcesInitiallyEnabled;
        @Nullable private final StateStore mStateStore;
        @Nullable private final QuotaManager mAnimationQuotaManager;
        @Nullable private final TimeGateway mTimeGateway;
        @Nullable private final SensorGateway mSensorGateway;

        Config(
                boolean platformDataSourcesInitiallyEnabled,
                @Nullable StateStore stateStore,
                @Nullable QuotaManager animationQuotaManager,
                @Nullable TimeGateway timeGateway,
                @Nullable SensorGateway sensorGateway) {
            this.mPlatformDataSourcesInitiallyEnabled = platformDataSourcesInitiallyEnabled;
            this.mStateStore = stateStore;
            this.mAnimationQuotaManager = animationQuotaManager;
            this.mTimeGateway = timeGateway;
            this.mSensorGateway = sensorGateway;
        }

        /** Builds a {@link DynamicTypeEvaluator.Config}. */
        public static final class Builder {
            private boolean mPlatformDataSourcesInitiallyEnabled = false;
            @Nullable private StateStore mStateStore = null;
            @Nullable private QuotaManager mAnimationQuotaManager = null;
            @Nullable private TimeGateway mTimeGateway = null;
            @Nullable private SensorGateway mSensorGateway = null;

            /**
             * Sets 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()}.
             *
             * <p>Defaults to {@code false}.
             */
            @NonNull
            public Builder setPlatformDataSourcesInitiallyEnabled(boolean value) {
                mPlatformDataSourcesInitiallyEnabled = value;
                return this;
            }

            /**
             * Sets the state store that will be used for dereferencing the state keys in the
             * dynamic types.
             *
             * <p>If not set, it's the equivalent of setting an empty state store (state bindings
             * will trigger {@link DynamicTypeValueReceiver#onInvalidated()}).
             */
            @NonNull
            public Builder setStateStore(@NonNull StateStore value) {
                mStateStore = value;
                return this;
            }

            /**
             * Sets the quota manager used for limiting the number of concurrently running
             * animations.
             *
             * <p>If not set, animations are disabled and non-infinite animations will have the end
             * value immediately.
             */
            @NonNull
            public Builder setAnimationQuotaManager(@NonNull QuotaManager value) {
                mAnimationQuotaManager = value;
                return this;
            }

            /**
             * Sets the gateway used for time data.
             *
             * <p>If not set, a default 1hz {@link TimeGateway} implementation that utilizes a
             * main-thread {@code Handler} to trigger is used.
             */
            @NonNull
            public Builder setTimeGateway(@NonNull TimeGateway value) {
                mTimeGateway = value;
                return this;
            }

            /**
             * Sets the gateway used for sensor data.
             *
             * <p>If not set, sensor data will not be available (sensor bindings will trigger {@link
             * DynamicTypeValueReceiver#onInvalidated()}).
             */
            @NonNull
            public Builder setSensorGateway(@NonNull SensorGateway value) {
                mSensorGateway = value;
                return this;
            }

            @NonNull
            public Config build() {
                return new Config(
                        mPlatformDataSourcesInitiallyEnabled,
                        mStateStore,
                        mAnimationQuotaManager,
                        mTimeGateway,
                        mSensorGateway);
            }
        }

        /**
         * Gets 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()}.
         */
        public boolean isPlatformDataSourcesInitiallyEnabled() {
            return mPlatformDataSourcesInitiallyEnabled;
        }

        /**
         * Gets the state store that will be used for dereferencing the state keys in the dynamic
         * types, or {@code null} which is equivalent to an empty state store (state bindings will
         * trigger {@link DynamicTypeValueReceiver#onInvalidated()}).
         */
        @Nullable
        public StateStore getStateStore() {
            return mStateStore;
        }

        /**
         * Gets the quota manager used for limiting the number of concurrently running animations,
         * or {@code null} if animations are disabled, causing non-infinite animations to have to
         * the end value immediately.
         */
        @Nullable
        public QuotaManager getAnimationQuotaManager() {
            return mAnimationQuotaManager;
        }

        /**
         * Gets the gateway used for sensor data, or {@code null} if sensor data is unavailable
         * (sensor bindings will trigger {@link DynamicTypeValueReceiver#onInvalidated()}).
         */
        @Nullable
        public SensorGateway getSensorGateway() {
            return mSensorGateway;
        }

        /**
         * Gets the gateway used for time data, or {@code null} if a default 1hz {@link TimeGateway}
         * that utilizes a main-thread {@code Handler} to trigger is used.
         */
        @Nullable
        public TimeGateway getTimeGateway() {
            return mTimeGateway;
        }
    }

    /** Constructs a {@link DynamicTypeEvaluator}. */
    public DynamicTypeEvaluator(@NonNull Config config) {
        this.mConfig = config;
        this.mStateStore =
                config.getStateStore() != null ? config.getStateStore() : EMPTY_STATE_STORE;
        this.mAnimationQuotaManager =
                config.getAnimationQuotaManager() != null
                        ? config.getAnimationQuotaManager()
                        : DISABLED_ANIMATIONS_QUOTA_MANAGER;
        Handler uiHandler = new Handler(Looper.getMainLooper());
        MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
        this.mTimeGateway =
                config.getTimeGateway() != null
                        ? config.getTimeGateway()
                        : new TimeGatewayImpl(uiHandler);
        this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, mTimeGateway);
        if (config.isPlatformDataSourcesInitiallyEnabled()
                && this.mTimeGateway instanceof TimeGatewayImpl) {
            ((TimeGatewayImpl) this.mTimeGateway).enableUpdates();
        }
        if (config.getSensorGateway() != null) {
            if (config.isPlatformDataSourcesInitiallyEnabled()) {
                config.getSensorGateway().enableUpdates();
            } else {
                config.getSensorGateway().disableUpdates();
            }
            this.mSensorGatewayDataSource =
                    new SensorGatewayPlatformDataSource(uiExecutor, config.getSensorGateway());
        } else {
            this.mSensorGatewayDataSource = null;
        }
    }

    /**
     * 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,
                new DynamicTypeValueReceiverOnExecutor<>(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, new DynamicTypeValueReceiverOnExecutor<>(consumer), resultBuilder);
        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.
     */
    @NonNull
    @RestrictTo(Scope.LIBRARY_GROUP)
    public BoundDynamicType bind(
            @NonNull DynamicFloat floatSource, @NonNull DynamicTypeValueReceiver<Float> consumer) {
        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
        bindRecursively(
                floatSource, new DynamicTypeValueReceiverOnExecutor<>(consumer), resultBuilder);
        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, new DynamicTypeValueReceiverOnExecutor<>(consumer), resultBuilder);
        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, new DynamicTypeValueReceiverOnExecutor<>(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, new DynamicTypeValueReceiverOnExecutor<>(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, new DynamicTypeValueReceiverOnExecutor<>(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 DynamicTypeValueReceiverWithPreUpdate<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);
                    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);
                    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 DynamicTypeValueReceiverWithPreUpdate<Integer> consumer,
            @NonNull List<DynamicDataNode<?>> resultBuilder) {
        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);
                    bindRecursively(
                            int32Source.getArithmeticOperation().getInputRhs(),
                            arithmeticNode.getRhsIncomingCallback(),
                            resultBuilder);

                    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);
                    bindRecursively(
                            op.getValueIfFalse(),
                            conditionalNode.getFalseValueIncomingCallback(),
                            resultBuilder);

                    node = conditionalNode;
                    break;
                }
            case FLOAT_TO_INT:
                {
                    FloatToInt32Node conversionNode =
                            new FloatToInt32Node(int32Source.getFloatToInt(), consumer);
                    node = conversionNode;

                    bindRecursively(
                            int32Source.getFloatToInt().getInput(),
                            conversionNode.getIncomingCallback(),
                            resultBuilder);
                    break;
                }
            case DURATION_PART:
                {
                    GetDurationPartOpNode durationPartOpNode =
                            new GetDurationPartOpNode(int32Source.getDurationPart(), consumer);
                    node = durationPartOpNode;

                    bindRecursively(
                            int32Source.getDurationPart().getInput(),
                            durationPartOpNode.getIncomingCallback(),
                            resultBuilder);
                    break;
                }
            case ANIMATABLE_FIXED:

                // 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:
                // 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);
                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 DynamicTypeValueReceiverWithPreUpdate<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 DynamicTypeValueReceiverWithPreUpdate<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 DynamicTypeValueReceiverWithPreUpdate<Float> consumer,
            @NonNull List<DynamicDataNode<?>> resultBuilder) {
        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);
                    bindRecursively(
                            floatSource.getArithmeticOperation().getInputRhs(),
                            arithmeticNode.getRhsIncomingCallback(),
                            resultBuilder);

                    break;
                }
            case INT32_TO_FLOAT_OPERATION:
                {
                    Int32ToFloatNode toFloatNode = new Int32ToFloatNode(consumer);
                    node = toFloatNode;

                    bindRecursively(
                            floatSource.getInt32ToFloatOperation().getInput(),
                            toFloatNode.getIncomingCallback(),
                            resultBuilder);
                    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);
                    bindRecursively(
                            op.getValueIfFalse(),
                            conditionalNode.getFalseValueIncomingCallback(),
                            resultBuilder);

                    node = conditionalNode;
                    break;
                }
            case ANIMATABLE_FIXED:
                // 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:
                // 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);
                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 DynamicTypeValueReceiverWithPreUpdate<Integer> consumer,
            @NonNull List<DynamicDataNode<?>> resultBuilder) {
        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:
                // 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:
                // 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);
                break;
            case CONDITIONAL_OP:
                ConditionalOpNode<Integer> conditionalNode = new ConditionalOpNode<>(consumer);

                ConditionalColorOp op = colorSource.getConditionalOp();
                bindRecursively(
                        op.getCondition(),
                        conditionalNode.getConditionIncomingCallback(),
                        resultBuilder);
                bindRecursively(
                        op.getValueIfTrue(),
                        conditionalNode.getTrueValueIncomingCallback(),
                        resultBuilder);
                bindRecursively(
                        op.getValueIfFalse(),
                        conditionalNode.getFalseValueIncomingCallback(),
                        resultBuilder);

                node = conditionalNode;
                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 DynamicTypeValueReceiverWithPreUpdate<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);
                    bindRecursively(
                            boolSource.getInt32Comparison().getInputRhs(),
                            compNode.getRhsIncomingCallback(),
                            resultBuilder);

                    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);
                    bindRecursively(
                            boolSource.getFloatComparison().getInputRhs(),
                            compNode.getRhsIncomingCallback(),
                            resultBuilder);

                    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 (this.mTimeGateway instanceof TimeGatewayImpl) {
            ((TimeGatewayImpl) mTimeGateway).enableUpdates();
        }
        if (mConfig.getSensorGateway() != null) {
            mConfig.getSensorGateway().enableUpdates();
        }
    }

    /** Disables sending updates on sensor and time. */
    @UiThread
    public void disablePlatformDataSources() {
        if (this.mTimeGateway instanceof TimeGatewayImpl) {
            ((TimeGatewayImpl) mTimeGateway).disableUpdates();
        }
        if (mConfig.getSensorGateway() != null) {
            mConfig.getSensorGateway().disableUpdates();
        }
    }

    /**
     * Closes resources owned by this {@link DynamicTypeEvaluator}.
     *
     * <p>This will not close provided resources, like the {@link TimeGateway} or {@link
     * SensorGateway}.
     */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @Override
    public void close() {
        if (mTimeGateway instanceof TimeGatewayImpl) {
            try {
                ((TimeGatewayImpl) 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 DynamicTypeValueReceiverWithPreUpdate<T> {

        @NonNull private final Executor mExecutor;
        @NonNull private final DynamicTypeValueReceiver<T> mConsumer;

        DynamicTypeValueReceiverOnExecutor(@NonNull DynamicTypeValueReceiver<T> consumer) {
            this(Runnable::run, consumer);
        }

        DynamicTypeValueReceiverOnExecutor(
                @NonNull Executor executor, @NonNull DynamicTypeValueReceiver<T> consumer) {
            this.mConsumer = consumer;
            this.mExecutor = executor;
        }

        /** This method is noop in this class. */
        @Override
        @SuppressWarnings("ExecutorTaskName")
        public void 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);
        }
    }
}