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 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);
        }
    }
}