public class

ProtoLayoutDynamicDataPipeline

extends java.lang.Object

 java.lang.Object

↳androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline

Gradle dependencies

compile group: 'androidx.wear.protolayout', name: 'protolayout-renderer', version: '1.2.0'

  • groupId: androidx.wear.protolayout
  • artifactId: protolayout-renderer
  • version: 1.2.0

Artifact androidx.wear.protolayout:protolayout-renderer:1.2.0 it located at Google repository (https://maven.google.com/)

Overview

Pipeline for dynamic data.

Given a dynamic ProtoLayout data source, this builds up a BoundDynamicType, which can source the required data, and transform it into its final form.

Summary

Constructors
publicProtoLayoutDynamicDataPipeline(java.util.Map<PlatformDataProvider, java.util.Set> platformDataProviders, StateStore stateStore)

Creates a ProtoLayoutDynamicDataPipeline without animation support.

publicProtoLayoutDynamicDataPipeline(java.util.Map<PlatformDataProvider, java.util.Set> platformDataProviders, StateStore stateStore, QuotaManager animationQuotaManager, QuotaManager dynamicNodesQuotaManager)

Creates a ProtoLayoutDynamicDataPipeline with animation support.

Methods
public voidclose()

Closes existing gateways.

public intgetDynamicExpressionsNodesCost()

Returns the cost of nodes existing in the pipeline.

public intgetRunningAnimationsCount()

Returns how many animations are running.

public java.lang.LonggetSeekableAnimationTotalDurationMillis(java.lang.String sourceKey)

Returns the total duration in milliseconds of the animated drawable associated with a StateSource with the given key name; or null if no such SourceKey exists.

public voidinitWithoutContentTransition()

Initialize the data pipeline without playing content transition animations.

public booleanisAllQuotaReleased()

Returns whether all quota has been released.

public ProtoLayoutDynamicDataPipeline.PipelineMakernewPipelineMaker()

Test version of the ProtoLayoutDynamicDataPipeline.newPipelineMaker(BiFunction, BiFunction) without animation inflators.

public ProtoLayoutDynamicDataPipeline.PipelineMakernewPipelineMaker(java.util.function.BiFunction<ModifiersProto.EnterTransition, View, AnimationSet> enterAnimationInflator, java.util.function.BiFunction<ModifiersProto.ExitTransition, View, AnimationSet> exitAnimationInflator)

Build ProtoLayoutDynamicDataPipeline.PipelineMaker.

public voidplayAvdAnimations(TriggerProto.Trigger.InnerCase triggerCase)

Play the animation with the given trigger type.

public voidresetAvdAnimations(TriggerProto.Trigger.InnerCase triggerCase)

Reset the avd animations with the given trigger type.

public voidsetFullyVisible(boolean fullyVisible)

Sets visibility for resources tracked by the pipeline and plays / stops any affected animations.

public voidsetUpdatesEnabled(boolean canUpdate)

Sets whether this proto layout can perform updates.

public intsize()

Returns the number of active dynamic types in this pipeline.

public voidstopAvdAnimations(TriggerProto.Trigger.InnerCase triggerCase)

Stops running avd animations and releases their quota.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public ProtoLayoutDynamicDataPipeline(java.util.Map<PlatformDataProvider, java.util.Set> platformDataProviders, StateStore stateStore)

Creates a ProtoLayoutDynamicDataPipeline without animation support.

public ProtoLayoutDynamicDataPipeline(java.util.Map<PlatformDataProvider, java.util.Set> platformDataProviders, StateStore stateStore, QuotaManager animationQuotaManager, QuotaManager dynamicNodesQuotaManager)

Creates a ProtoLayoutDynamicDataPipeline with animation support. Maximum number of concurrently running animations is defined in the given QuotaManager.

Methods

public int size()

Returns the number of active dynamic types in this pipeline.

public ProtoLayoutDynamicDataPipeline.PipelineMaker newPipelineMaker(java.util.function.BiFunction<ModifiersProto.EnterTransition, View, AnimationSet> enterAnimationInflator, java.util.function.BiFunction<ModifiersProto.ExitTransition, View, AnimationSet> exitAnimationInflator)

Build ProtoLayoutDynamicDataPipeline.PipelineMaker.

Test version of the ProtoLayoutDynamicDataPipeline.newPipelineMaker(BiFunction, BiFunction) without animation inflators.

public void setUpdatesEnabled(boolean canUpdate)

Sets whether this proto layout can perform updates. If the proto layout cannot update, then updates through the data pipeline (e.g. health updates) will be suppressed.

public void close()

Closes existing gateways.

public void initWithoutContentTransition()

Initialize the data pipeline without playing content transition animations. Normally this is called automatically when the parent ViewGroup associated with this pipeline is attached to a View hierarchy. This is so that the content transition animations can be executed before this (if needed).

This method can be called directly in screenshot tests and when the renderer output is never supposed to be attached to a window.

public void playAvdAnimations(TriggerProto.Trigger.InnerCase triggerCase)

Play the animation with the given trigger type.

public void resetAvdAnimations(TriggerProto.Trigger.InnerCase triggerCase)

Reset the avd animations with the given trigger type.

public void stopAvdAnimations(TriggerProto.Trigger.InnerCase triggerCase)

Stops running avd animations and releases their quota.

public void setFullyVisible(boolean fullyVisible)

Sets visibility for resources tracked by the pipeline and plays / stops any affected animations.

public java.lang.Long getSeekableAnimationTotalDurationMillis(java.lang.String sourceKey)

Returns the total duration in milliseconds of the animated drawable associated with a StateSource with the given key name; or null if no such SourceKey exists.

public int getRunningAnimationsCount()

Returns how many animations are running.

public int getDynamicExpressionsNodesCost()

Returns the cost of nodes existing in the pipeline.

public boolean isAllQuotaReleased()

Returns whether all quota has been released.

Source

/*
 * Copyright 2023 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.renderer.dynamicdata;

import static androidx.core.util.Preconditions.checkNotNull;

import static java.lang.Math.max;
import static java.lang.Math.min;

import android.annotation.SuppressLint;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.icu.util.ULocale;
import android.util.Log;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.animation.AnimationSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.vectordrawable.graphics.drawable.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.expression.PlatformDataKey;
import androidx.wear.protolayout.expression.pipeline.BoundDynamicType;
import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest;
import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator;
import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator.EvaluationException;
import androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver;
import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider;
import androidx.wear.protolayout.expression.pipeline.PlatformTimeUpdateNotifierImpl;
import androidx.wear.protolayout.expression.pipeline.QuotaManager;
import androidx.wear.protolayout.expression.pipeline.StateStore;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicBool;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicColor;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicFloat;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
import androidx.wear.protolayout.proto.ColorProto.ColorProp;
import androidx.wear.protolayout.proto.DimensionProto.DegreesProp;
import androidx.wear.protolayout.proto.DimensionProto.DpProp;
import androidx.wear.protolayout.proto.ModifiersProto.AnimatedVisibility;
import androidx.wear.protolayout.proto.ModifiersProto.EnterTransition;
import androidx.wear.protolayout.proto.ModifiersProto.ExitTransition;
import androidx.wear.protolayout.proto.TriggerProto.Trigger;
import androidx.wear.protolayout.proto.TypesProto.BoolProp;
import androidx.wear.protolayout.renderer.dynamicdata.NodeInfo.ResolvedAvd;

import com.google.common.collect.ImmutableList;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * Pipeline for dynamic data.
 *
 * <p>Given a dynamic ProtoLayout data source, this builds up a {@link BoundDynamicType}, which can
 * source the required data, and transform it into its final form.
 */
@RestrictTo(Scope.LIBRARY_GROUP)
public class ProtoLayoutDynamicDataPipeline {
    @NonNull private static final String TAG = "DynamicDataPipeline";

    @NonNull
    private static final QuotaManager DISABLED_ANIMATIONS_QUOTA_MANAGER =
            new FixedQuotaManagerImpl(/* quotaCap= */ 0, "disabled animations");

    @NonNull final PositionIdTree<NodeInfo> mPositionIdTree = new PositionIdTree<>();
    @NonNull final List<QuotaAwareAnimationSet> mEnterAnimations = new ArrayList<>();
    @NonNull final List<QuotaAwareAnimationSet> mExitAnimations = new ArrayList<>();
    final boolean mEnableAnimations;
    boolean mFullyVisible;
    @NonNull final QuotaManager mAnimationQuotaManager;
    @NonNull private final DynamicTypeEvaluator mEvaluator;
    @NonNull private final PlatformTimeUpdateNotifierImpl mTimeNotifier;

    /** Creates a {@link ProtoLayoutDynamicDataPipeline} without animation support. */
    @RestrictTo(Scope.LIBRARY_GROUP)
    public ProtoLayoutDynamicDataPipeline(
            @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders,
            @NonNull StateStore stateStore) {
        // Build pipeline with quota that doesn't allow any animations.
        this(
                platformDataProviders,
                stateStore,
                /* enableAnimations= */ false,
                DISABLED_ANIMATIONS_QUOTA_MANAGER,
                new FixedQuotaManagerImpl(Integer.MAX_VALUE));
    }

    /**
     * Creates a {@link ProtoLayoutDynamicDataPipeline} with animation support. Maximum number of
     * concurrently running animations is defined in the given {@link QuotaManager}.
     */
    @RestrictTo(Scope.LIBRARY_GROUP)
    public ProtoLayoutDynamicDataPipeline(
            @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders,
            @NonNull StateStore stateStore,
            @NonNull QuotaManager animationQuotaManager,
            @NonNull QuotaManager dynamicNodesQuotaManager) {
        this(
                platformDataProviders,
                stateStore,
                /* enableAnimations= */ true,
                animationQuotaManager,
                dynamicNodesQuotaManager);
    }

    /** Creates a {@link ProtoLayoutDynamicDataPipeline}. */
    private ProtoLayoutDynamicDataPipeline(
            @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders,
            @NonNull StateStore stateStore,
            boolean enableAnimations,
            @NonNull QuotaManager animationQuotaManager,
            @NonNull QuotaManager dynamicNodeQuotaManager) {
        this.mEnableAnimations = enableAnimations;
        this.mAnimationQuotaManager = animationQuotaManager;
        DynamicTypeEvaluator.Config.Builder evaluatorConfigBuilder =
                new DynamicTypeEvaluator.Config.Builder().setStateStore(stateStore);
        evaluatorConfigBuilder.setDynamicTypesQuotaManager(dynamicNodeQuotaManager);
        for (Map.Entry<PlatformDataProvider, Set<PlatformDataKey<?>>> providerEntry :
                platformDataProviders.entrySet()) {
            evaluatorConfigBuilder.addPlatformDataProvider(
                    providerEntry.getKey(), providerEntry.getValue());
        }
        this.mTimeNotifier = new PlatformTimeUpdateNotifierImpl();

        evaluatorConfigBuilder.setPlatformTimeUpdateNotifier(this.mTimeNotifier);
        mTimeNotifier.setUpdatesEnabled(true);

        if (enableAnimations) {
            evaluatorConfigBuilder.setAnimationQuotaManager(animationQuotaManager);
        }
        this.mEvaluator = new DynamicTypeEvaluator(evaluatorConfigBuilder.build());
    }

    /** Returns the number of active dynamic types in this pipeline. */
    @VisibleForTesting
    public int size() {
        return mPositionIdTree.getAllNodes().stream().mapToInt(NodeInfo::size).sum();
    }

    @UiThread
    void clear() {
        mPositionIdTree.clear();
    }

    /** Removes all nodes that are descendants of {@code posId}. */
    @UiThread
    void removeChildNodesFor(@NonNull String posId) {
        mPositionIdTree.removeChildNodesFor(posId);
    }

    /** Build {@link PipelineMaker}. */
    @NonNull
    @RestrictTo(Scope.LIBRARY_GROUP)
    public PipelineMaker newPipelineMaker(
            @NonNull BiFunction<EnterTransition, View, AnimationSet> enterAnimationInflator,
            @NonNull BiFunction<ExitTransition, View, AnimationSet> exitAnimationInflator) {
        return new PipelineMaker(this, enterAnimationInflator, exitAnimationInflator, mEvaluator);
    }

    /**
     * Test version of the {@link #newPipelineMaker(BiFunction, BiFunction)} without animation
     * inflators.
     */
    @VisibleForTesting
    @NonNull
    public PipelineMaker newPipelineMaker() {
        return newPipelineMaker(
                (enterTransition, view) -> new AnimationSet(/* shareInterpolator= */ false),
                (exitTransition, view) -> new AnimationSet(/* shareInterpolator= */ false));
    }

    /**
     * Sets whether this proto layout can perform updates. If the proto layout cannot update, then
     * updates through the data pipeline (e.g. health updates) will be suppressed.
     */
    @UiThread
    @SuppressWarnings("RestrictTo")
    @RestrictTo(Scope.LIBRARY_GROUP)
    public void setUpdatesEnabled(boolean canUpdate) {
        mTimeNotifier.setUpdatesEnabled(canUpdate);
    }

    /** Closes existing gateways. */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @SuppressWarnings("RestrictTo")
    public void close() {
        mPositionIdTree.clear();
        mTimeNotifier.setUpdatesEnabled(false);
    }

    /**
     * PipelineMaker for a dynamic data pipeline.
     *
     * <p>Given a dynamic ProtoLayout data source, this creates a sequence of {@link
     * BoundDynamicType} instances, which can source the required data, and transform it into its
     * final form.
     *
     * <p>The nodes are accumulated and can be committed to the pipeline.
     *
     * <p>Note that this class is not thread-safe.
     */
    @RestrictTo(Scope.LIBRARY_GROUP)
    public static final class PipelineMaker {
        @NonNull private final ProtoLayoutDynamicDataPipeline mPipeline;

        @NonNull
        private final BiFunction<EnterTransition, View, AnimationSet> mEnterAnimationInflator;

        @NonNull
        private final BiFunction<ExitTransition, View, AnimationSet> mExitAnimationInflator;

        // Stores pending nodes that are committed to the pipeline after a successful layout update.
        @NonNull private final Map<String, NodeInfo> mPosIdToNodeInfo = new ArrayMap<>();
        @NonNull private final List<String> mNodesPendingChildrenRemoval = new ArrayList<>();
        @NonNull private final Set<String> mChangedNodes = new ArraySet<>();
        @NonNull private final Set<String> mParentsOfChangedNodes = new ArraySet<>();
        @NonNull private final DynamicTypeEvaluator mEvaluator;
        private int mExitAnimationsCounter = 0;

        PipelineMaker(
                @NonNull ProtoLayoutDynamicDataPipeline pipeline,
                @NonNull BiFunction<EnterTransition, View, AnimationSet> enterAnimationInflator,
                @NonNull BiFunction<ExitTransition, View, AnimationSet> exitAnimationInflator,
                @NonNull DynamicTypeEvaluator evaluator) {
            this.mPipeline = pipeline;
            this.mEnterAnimationInflator = enterAnimationInflator;
            this.mExitAnimationInflator = exitAnimationInflator;
            this.mEvaluator = evaluator;
        }

        /**
         * Clears the current data in the {@link ProtoLayoutDynamicDataPipeline} instance that was
         * used to create this and then commits any stored changes.
         *
         * @param inflatedParent The renderer-owned parent view for all of the layout elements
         *     associated with the nodes in this pipeline. This will be used for content transition
         *     animations.
         * @param isReattaching if True, this layout is being reattached and will skip content
         *     transition animations.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @UiThread
        public void clearDataPipelineAndCommit(
                @NonNull ViewGroup inflatedParent, boolean isReattaching) {
            this.mPipeline.clear();
            this.commit(inflatedParent, isReattaching);
        }

        /**
         * Plays Exit animations. This method should be called while {@code parentView} still
         * corresponds to the previous layout. Any subsequent change to the layout should be
         * schedule through the {@code onEnd} callback.
         *
         * @param parentView The parent view these nodes are being inflated into. Note that it
         *     should be attached to a window (and has gone through its layout passes).
         * @param isReattaching if True, this layout is being reattached and will skip content
         *     transition animations.
         * @param onEnd the callback to execute after all Exit animations have finished.
         */
        @UiThread
        @RestrictTo(Scope.LIBRARY_GROUP)
        public void playExitAnimations(
                @NonNull ViewGroup parentView, boolean isReattaching, @Nullable Runnable onEnd) {
            mPipeline.cancelContentTransitionAnimations();

            // This is needed because onEnd should be called only once. In case that there are exit
            // animations that can't be played due to no quota, QuotaAwaraAnimationSet will try to
            // play it, fail and call onEnd. However, the counter of number of played animation will
            // stay 0, so the outer condition will also be true and onEnd will be called again.
            AtomicBoolean onEndWasCalled = new AtomicBoolean(false);
            Runnable wrappedOnEnd =
                    onEnd != null
                            ? () -> {
                                if (!onEndWasCalled.getAndSet(true)) {
                                    onEnd.run();
                                }
                            }
                            : null;

            if (!isReattaching && mPipeline.mFullyVisible && mPipeline.mEnableAnimations) {
                Map<String, ExitTransition> animatingNodes = new ArrayMap<>();
                for (String parentOfChangedNodes : mParentsOfChangedNodes) {
                    mPipeline
                            .mPositionIdTree
                            .findChildrenFor(parentOfChangedNodes)
                            .forEach(
                                    node ->
                                            addAffectedExitAnimations(
                                                    node.getPosId(), animatingNodes));
                }
                for (String changedNode : mChangedNodes) {
                    addAffectedExitAnimations(changedNode, animatingNodes);
                }
                mExitAnimationsCounter = 0;
                for (Map.Entry<String, ExitTransition> animatingNode : animatingNodes.entrySet()) {
                    View associatedView = parentView.findViewWithTag(animatingNode.getKey());
                    if (associatedView != null) {
                        AnimationSet animationSet =
                                mExitAnimationInflator.apply(
                                        checkNotNull(animatingNode.getValue()), associatedView);
                        if (animationSet != null && !animationSet.getAnimations().isEmpty()) {
                            QuotaAwareAnimationSet quotaAwareAnimationSet =
                                    new QuotaAwareAnimationSet(
                                            animationSet,
                                            mPipeline.mAnimationQuotaManager,
                                            associatedView,
                                            () -> {
                                                if (wrappedOnEnd != null) {
                                                    mExitAnimationsCounter--;
                                                    if (mExitAnimationsCounter == 0) {
                                                        mPipeline.mExitAnimations.clear();
                                                        wrappedOnEnd.run();
                                                    }
                                                }
                                            });
                            quotaAwareAnimationSet.tryStartAnimation(
                                    () -> {
                                        mExitAnimationsCounter++;
                                        mPipeline.mExitAnimations.add(quotaAwareAnimationSet);
                                    });
                        }
                    }
                }
            }
            if (mPipeline.mExitAnimations.isEmpty() && wrappedOnEnd != null) {
                // No exit animations.
                wrappedOnEnd.run();
            }
        }

        private void addAffectedExitAnimations(
                @NonNull String changedNode, @NonNull Map<String, ExitTransition> animatingNodes) {
            List<NodeInfo> nodesAffectedBy =
                    mPipeline.getNodesAffectedBy(
                            changedNode,
                            node -> {
                                AnimatedVisibility animatedVisibility =
                                        node.getAnimatedVisibility();
                                return animatedVisibility != null
                                        && animatedVisibility.hasExitTransition();
                            });
            for (NodeInfo affectedNode : nodesAffectedBy) {
                animatingNodes.putIfAbsent(
                        affectedNode.getPosId(),
                        checkNotNull(affectedNode.getAnimatedVisibility()).getExitTransition());
            }
        }

        /**
         * Commits any stored changes into the {@link ProtoLayoutDynamicDataPipeline} instance that
         * was used to create this. This replaces any already available node and should be called
         * only once per layout update.
         *
         * @param inflatedParent The parent view these nodes are being inflated into. This will be
         *     used for Enter animations. If this view is not attached to a window, the animations
         *     (and the rest of pipeline init) will be scheduled to run when the view attaches to a
         *     window later
         * @param isReattaching if True, this layout is being reattached and will skip content
         *     transition animations.
         */
        @UiThread
        @RestrictTo(Scope.LIBRARY_GROUP)
        public void commit(@NonNull ViewGroup inflatedParent, boolean isReattaching) {
            for (String nodePosId : mNodesPendingChildrenRemoval) {
                mPipeline.removeChildNodesFor(nodePosId);
            }
            mNodesPendingChildrenRemoval.clear();
            for (Entry<String, NodeInfo> entry : mPosIdToNodeInfo.entrySet()) {
                String key = entry.getKey();
                if (key.isEmpty()) {
                    Log.e(TAG, "Ignoring empty posId.");
                    continue;
                }
                mPipeline.mPositionIdTree.addOrReplace(key, entry.getValue());
            }

            // in the modified levels.
            if (isReattaching || !mPipeline.mFullyVisible) {
                // Skip content transition animations.
                mChangedNodes.clear();
            }

            // Capture nodes with EnterTransition animation.
            Map<String, EnterTransition> enterTransitionNodes = new ArrayMap<>();
            boolean hasSlideInAnimation = false;
            if (mPipeline.mEnableAnimations) {
                for (String changedNode : mChangedNodes) {
                    List<NodeInfo> nodesAffectedBy =
                            mPipeline.getNodesAffectedBy(
                                    changedNode,
                                    node -> {
                                        AnimatedVisibility animatedVisibility =
                                                node.getAnimatedVisibility();
                                        return animatedVisibility != null
                                                && animatedVisibility.hasEnterTransition();
                                    });
                    for (NodeInfo affectedNode : nodesAffectedBy) {
                        EnterTransition enterTransition =
                                checkNotNull(affectedNode.getAnimatedVisibility())
                                        .getEnterTransition();
                        enterTransitionNodes.putIfAbsent(affectedNode.getPosId(), enterTransition);
                        hasSlideInAnimation |= enterTransition.hasSlideIn();
                    }
                }
            }

            Runnable initLayoutRunnable =
                    () -> {
                        mPipeline.initNewLayout();
                        playEnterAnimations(inflatedParent, isReattaching, enterTransitionNodes);
                    };

            // Slide animations need to know the new measurements of the view in order to calculate
            // start and end positions, so we force a measure pass.
            if (hasSlideInAnimation) {
                // The GlobalLayoutListener ensures that initLayoutRunnable will run after the
                // measure pass has finished.
                ViewTreeObserver viewTreeObserver = inflatedParent.getViewTreeObserver();
                viewTreeObserver.addOnGlobalLayoutListener(
                        new OnGlobalLayoutListener() {
                            @Override
                            public void onGlobalLayout() {
                                if (viewTreeObserver.isAlive()) {
                                    viewTreeObserver.removeOnGlobalLayoutListener(this);
                                    initLayoutRunnable.run();
                                }
                            }
                        });
                inflatedParent.measure(
                        MeasureSpec.makeMeasureSpec(
                                inflatedParent.getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(
                                inflatedParent.getMeasuredHeight(), MeasureSpec.EXACTLY));
            } else {
                initLayoutRunnable.run();
            }
        }

        @UiThread
        private void playEnterAnimations(
                @NonNull ViewGroup parentView,
                boolean isReattaching,
                Map<String, EnterTransition> animatingNodes) {
            // Cancel any already running Enter animation.
            mPipeline.mEnterAnimations.forEach(QuotaAwareAnimationSet::cancelAnimations);
            mPipeline.mEnterAnimations.clear();

            if (isReattaching || !mPipeline.mFullyVisible || !mPipeline.mEnableAnimations) {
                return;
            }
            for (Map.Entry<String, EnterTransition> animatingNode : animatingNodes.entrySet()) {
                View associatedView = parentView.findViewWithTag(animatingNode.getKey());
                if (associatedView != null) {
                    AnimationSet animationSet =
                            mEnterAnimationInflator.apply(
                                    checkNotNull(animatingNode.getValue()), associatedView);

                    if (animationSet != null && !animationSet.getAnimations().isEmpty()) {
                        QuotaAwareAnimationSet quotaAwareAnimationSet =
                                new QuotaAwareAnimationSet(
                                        animationSet,
                                        mPipeline.mAnimationQuotaManager,
                                        associatedView);
                        quotaAwareAnimationSet.tryStartAnimation(
                                () -> mPipeline.mEnterAnimations.add(quotaAwareAnimationSet));
                    }
                }
            }
        }

        @NonNull
        private NodeInfo getNodeInfo(@NonNull String posId) {
            return mPosIdToNodeInfo.computeIfAbsent(
                    posId, k -> new NodeInfo(posId, mPipeline.mAnimationQuotaManager));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicString stringSource,
                @NonNull Locale locale,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<String> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicStringInternal(
                            stringSource, ULocale.forLocale(locale), consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicInt32 int32Source,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Integer> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicInt32Internal(int32Source, consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicString stringSource,
                @NonNull String invalidData,
                @NonNull Locale locale,
                @NonNull String posId,
                @NonNull Consumer<String> consumer) {
            return addPipelineFor(
                    stringSource, locale, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicFloat floatSource,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Float> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicFloatInternal(floatSource, consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicFloat floatSource,
                float invalidData,
                @NonNull String posId,
                @NonNull Consumer<Float> consumer) {
            return addPipelineFor(
                    floatSource, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicColor colorSource,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Integer> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicColorInternal(colorSource, consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicBool boolSource,
                @NonNull String posId,
                @NonNull Runnable triggerAnimationRunnable) {
            DynamicTypeValueReceiver<Boolean> consumer =
                    buildBooleanConditionTriggerCallback(
                            triggerAnimationRunnable, mPipeline.mAnimationQuotaManager);
            return addPipelineFor(boolSource, posId, consumer);
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicBool boolSource,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicBoolInternal(boolSource, consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DynamicBool boolSource,
                boolean invalidData,
                @NonNull String posId,
                @NonNull Consumer<Boolean> consumer) {
            return addPipelineFor(
                    boolSource, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @NonNull
        @RestrictTo(Scope.LIBRARY_GROUP)
        public PipelineMaker addPipelineFor(
                @NonNull DpProp dpProp,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Float> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicFloatInternal(
                            dpProp.getDynamicValue(), consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DegreesProp degreesProp,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Float> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicFloatInternal(
                            degreesProp.getDynamicValue(), consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull ColorProp colorProp,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Integer> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicColorInternal(
                            colorProp.getDynamicValue(), consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull BoolProp boolProp,
                @NonNull String posId,
                @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
            DynamicTypeBindingRequest bindingRequest =
                    DynamicTypeBindingRequest.forDynamicBoolInternal(
                            boolProp.getDynamicValue(), consumer);
            tryBindRequest(posId, bindingRequest, consumer::onInvalidated);
            return this;
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DpProp dpProp,
                float invalidData,
                @NonNull String posId,
                @NonNull Consumer<Float> consumer) {
            return addPipelineFor(dpProp, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull DegreesProp degreesProp,
                float invalidData,
                @NonNull String posId,
                @NonNull Consumer<Float> consumer) {
            return addPipelineFor(
                    degreesProp, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull ColorProp colorProp,
                int invalidData,
                @NonNull String posId,
                @NonNull Consumer<Integer> consumer) {
            return addPipelineFor(
                    colorProp, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressWarnings("RestrictTo")
        @NonNull
        public PipelineMaker addPipelineFor(
                @NonNull BoolProp boolProp,
                boolean invalidData,
                @NonNull String posId,
                @NonNull Consumer<Boolean> consumer) {
            return addPipelineFor(boolProp, posId, buildStateUpdateCallback(invalidData, consumer));
        }

        private void tryBindRequest(
                String posId, DynamicTypeBindingRequest request, Runnable onFailure) {
            BoundDynamicType dynamicType = null;
            NodeInfo nodeInfo = getNodeInfo(posId);
            try {
                dynamicType = mEvaluator.bind(request);
                nodeInfo.addBoundType(dynamicType);
            } catch (EvaluationException exception) {
                Log.e(TAG, "Fails to bind dynamicType.", exception);
                nodeInfo.addFailedBindingRequest(request);
                onFailure.run();
            }
        }

        /** This store method shall be called during the layout inflation in a background thread. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @SuppressLint("CheckReturnValue") // (b/247804720)
        @NonNull
        public PipelineMaker addResolvedAnimatedImage(
                @NonNull AnimatedVectorDrawable drawable,
                @NonNull Trigger trigger,
                @NonNull String posId) {
            if (!this.mPipeline.mEnableAnimations) {
                Log.w(TAG, "Cannot use ResolvedAnimationImage; animations are disabled.");
                return this;
            }

            getNodeInfo(posId).addResolvedAvd(drawable, trigger);
            return this;
        }

        /**
         * This store method shall be called during the layout inflation in a background thread. It
         * adds given {@link DynamicBool} to the pipeline too.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public PipelineMaker addResolvedAnimatedImageWithBoolTrigger(
                @NonNull AnimatedVectorDrawable drawable,
                @NonNull Trigger trigger,
                @NonNull String posId,
                @NonNull DynamicBool boolTrigger) {
            if (!this.mPipeline.mEnableAnimations) {
                Log.w(TAG, "Cannot use ResolvedAnimationImage; animations are disabled.");
                return this;
            }

            if (trigger.getInnerCase() != Trigger.InnerCase.ON_CONDITION_MET_TRIGGER) {
                Log.w(TAG, "Wrong trigger type.");
                return this;
            }

            ResolvedAvd avd = getNodeInfo(posId).addResolvedAvd(drawable, trigger);
            addPipelineFor(boolTrigger, posId, avd::startAnimation);
            return this;
        }

        /** This store method shall be called during the layout inflation in a background thread. */
        @NonNull
        @RestrictTo(Scope.LIBRARY_GROUP)
        public PipelineMaker addResolvedSeekableAnimatedImage(
                @NonNull SeekableAnimatedVectorDrawable seekableDrawable,
                @NonNull DynamicFloat boundProgress,
                @NonNull String posId) {
            if (!this.mPipeline.mEnableAnimations) {
                Log.w(TAG, "Cannot use ResolveSeekableAvd; animations are disabled.");
                return this;
            }

            // Register the bound progress to the seekable animated drawable.
            addPipelineFor(
                    boundProgress,
                    0.0f,
                    posId,
                    aFloat -> {
                        float progress = max(0.0f, min(aFloat, 1.0f));
                        seekableDrawable.setCurrentPlayTime(
                                (long) (progress * seekableDrawable.getTotalDuration()));
                    });
            getNodeInfo(posId)
                    .addResolvedSeekableAvd(
                            new NodeInfo.ResolvedSeekableAvd(seekableDrawable, boundProgress));
            return this;
        }

        /** Stores the {@link AnimatedVisibility} associated with the {@code posId}. */
        @NonNull
        @RestrictTo(Scope.LIBRARY_GROUP)
        public PipelineMaker storeAnimatedVisibilityFor(
                @NonNull String posId, @NonNull AnimatedVisibility animatedVisibility) {
            if (!mPipeline.mEnableAnimations) {
                Log.w(TAG, "Can't use AnimatedVisibility; animations are disabled.");
                return this;
            }
            getNodeInfo(posId).setAnimatedVisibility(animatedVisibility);
            return this;
        }

        /**
         * Mark the node {@code posId} as changed. Content transition animations affected by this
         * node will be triggered when the pipeline is committed.
         *
         * @param posId positionId of the node
         * @param includePreviousChildren if True, the previous children of this node will be marked
         *     as changed too. This is used for triggering Exit animations.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public PipelineMaker markNodeAsChanged(
                @NonNull String posId, boolean includePreviousChildren) {
            if (mPipeline.mEnableAnimations) {
                mChangedNodes.add(posId);
                mParentsOfChangedNodes.add(posId);
            }
            return this;
        }

        @NonNull
        private static DynamicTypeValueReceiver<Boolean> buildBooleanConditionTriggerCallback(
                @NonNull Runnable triggerAnimationRunnable, @NonNull QuotaManager quotaManager) {
            return new DynamicTypeValueReceiver<Boolean>() {
                private boolean mCurrent;

                @Override
                public void onData(@NonNull Boolean newData) {

                    if (newData && !mCurrent && quotaManager.tryAcquireQuota(1)) {
                        triggerAnimationRunnable.run();
                    }
                    mCurrent = newData;
                }

                @Override
                public void onInvalidated() {}
            };
        }

        @NonNull
        private <T> DynamicTypeValueReceiver<T> buildStateUpdateCallback(
                @NonNull T invalidData, @NonNull Consumer<T> consumer) {
            return new DynamicTypeValueReceiver<T>() {
                @Override
                public void onData(@NonNull T newData) {
                    consumer.accept(newData);
                }

                @Override
                public void onInvalidated() {
                    consumer.accept(invalidData);
                }
            };
        }

        /**
         * Add the given source to the pipeline for future evaluation. Evaluation will start when
         * {@link PipelineMaker} is committed with {@link PipelineMaker#commit}.
         */
        @NonNull
        @RestrictTo(Scope.LIBRARY_GROUP)
        public PipelineMaker markForChildRemoval(@NonNull String nodePosId) {
            mNodesPendingChildrenRemoval.add(nodePosId);
            return this;
        }

        /** Stores a node if doesn't exist. Otherwise does nothing. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public PipelineMaker rememberNode(@NonNull String nodePosId) {
            NodeInfo ignored = getNodeInfo(nodePosId);
            return this;
        }
    }

    /**
     * Initialize the data pipeline without playing content transition animations. Normally this is
     * called automatically when the parent {@link ViewGroup} associated with this pipeline is
     * attached to a {@link View} hierarchy. This is so that the content transition animations can
     * be executed before this (if needed).
     *
     * <p>This method can be called directly in screenshot tests and when the renderer output is
     * never supposed to be attached to a window.
     */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @UiThread
    public void initWithoutContentTransition() {
        initNewLayout();
    }

    /** Initialize the data pipeline after a new layout is pushed. */
    @UiThread
    @SuppressWarnings("RestrictTo")
    void initNewLayout() {
        if (mFullyVisible) {
            playAvdAnimations(Trigger.InnerCase.ON_VISIBLE_TRIGGER);
            playAvdAnimations(Trigger.InnerCase.ON_VISIBLE_ONCE_TRIGGER);
        }
        playAvdAnimations(Trigger.InnerCase.ON_LOAD_TRIGGER);
        setAnimationVisibility(mFullyVisible);

        // Retry failing binding requests
        mPositionIdTree.forEach(
                nodeInfo ->
                        nodeInfo.getFailedBindingRequest()
                                .removeIf(request -> retryBindingRequest(nodeInfo, request)));

        mPositionIdTree.forEach(NodeInfo::initPendingBoundTypes);
    }

    private boolean retryBindingRequest(NodeInfo nodeInfo, DynamicTypeBindingRequest request) {
        BoundDynamicType dynamicType = null;
        try {
            dynamicType = mEvaluator.bind(request);
            nodeInfo.addBoundType(dynamicType);
            return true;
        } catch (EvaluationException exception) {
            Log.v(TAG, "Retry to bind dynamicType failed.", exception);
        }
        return false;
    }

    /** Play the animation with the given trigger type. */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @VisibleForTesting
    public void playAvdAnimations(@NonNull Trigger.InnerCase triggerCase) {
        mPositionIdTree.forEach(info -> info.playAvdAnimations(triggerCase));
    }

    /** Sets visibility of animations. Also pauses or resumes any animators. */
    @UiThread
    private void setAnimationVisibility(boolean visible) {
        mPositionIdTree.forEach(info -> info.setVisibility(visible));
    }

    /** Reset the avd animations with the given trigger type. */
    @UiThread
    @VisibleForTesting
    @RestrictTo(Scope.LIBRARY_GROUP)
    public void resetAvdAnimations(@NonNull Trigger.InnerCase triggerCase) {
        mPositionIdTree.forEach(info -> info.resetAvdAnimations(triggerCase));
    }

    /** Stops running avd animations and releases their quota. */
    @UiThread
    @VisibleForTesting
    @RestrictTo(Scope.LIBRARY_GROUP)
    public void stopAvdAnimations(@NonNull Trigger.InnerCase triggerCase) {
        mPositionIdTree.forEach(info -> info.stopAvdAnimations(triggerCase));
    }

    /** Cancel any already running content transition animations. */
    @UiThread
    void cancelContentTransitionAnimations() {
        ImmutableList.copyOf(mExitAnimations).forEach(QuotaAwareAnimationSet::cancelAnimations);
        mExitAnimations.clear();
        mEnterAnimations.forEach(QuotaAwareAnimationSet::cancelAnimations);
        mEnterAnimations.clear();
    }

    /**
     * Sets visibility for resources tracked by the pipeline and plays / stops any affected
     * animations.
     */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @UiThread
    public void setFullyVisible(boolean fullyVisible) {
        if (this.mFullyVisible == fullyVisible) {
            return;
        }

        this.mFullyVisible = fullyVisible;
        // Set visibility to already started INFINITE AVD will pause the animation when the drawable
        // is invisible and resume the animation when becomes visible again.
        setAnimationVisibility(fullyVisible);
        if (fullyVisible) {
            playAvdAnimations(Trigger.InnerCase.ON_VISIBLE_TRIGGER);
            playAvdAnimations(Trigger.InnerCase.ON_VISIBLE_ONCE_TRIGGER);
        } else {
            cancelContentTransitionAnimations();
            // Stop the AVD animation with ON_VISIBLE_TRIGGER, but not stop AVD animations with
            // ON_VISIBLE_ONCE_TRIGGER and ON_LOAD_TRIGGER. AVD does not provide API to check
            // whether it is infinite, thus it is hard to stop finite animations only. For the AVDs
            // that would not get restarted, animations are not stopped when the layout becomes
            // invisible. Finite animations continue until they reach the end, while infinite
            // animations are paused by setting their visibility to false.
            stopAvdAnimations(Trigger.InnerCase.ON_VISIBLE_TRIGGER);
            resetAvdAnimations(Trigger.InnerCase.ON_VISIBLE_TRIGGER);
        }
    }

    /**
     * Returns the total duration in milliseconds of the animated drawable associated with a
     * StateSource with the given key name; or null if no such SourceKey exists.
     */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @Nullable
    public Long getSeekableAnimationTotalDurationMillis(@NonNull String sourceKey) {
        NodeInfo node =
                mPositionIdTree.findFirst(
                        nodeInfo ->
                                nodeInfo.getSeekableAnimationTotalDurationMillis(sourceKey)
                                        != null);
        if (node != null) {
            return node.getSeekableAnimationTotalDurationMillis(sourceKey);
        }
        return null;
    }

    /**
     * Returns the list of nodes with matching {@code predicate} affected by a change to the node
     * {@code posId}
     */
    @UiThread
    @NonNull
    List<NodeInfo> getNodesAffectedBy(
            @NonNull String posId, @NonNull Predicate<NodeInfo> predicate) {
        List<NodeInfo> affectedNodes = mPositionIdTree.findAncestorsFor(posId, predicate);
        NodeInfo currentNode = mPositionIdTree.get(posId);
        if (currentNode != null && predicate.test(currentNode)) {
            affectedNodes.add(currentNode);
        }

        return affectedNodes;
    }

    /** Returns how many animations are running. */
    @VisibleForTesting
    public int getRunningAnimationsCount() {
        return mPositionIdTree.getAllNodes().stream()
                        .mapToInt(NodeInfo::getRunningAnimationCount)
                        .sum()
                + mEnterAnimations.stream()
                        .mapToInt(QuotaAwareAnimationSet::getRunningAnimationCount)
                        .sum()
                + mExitAnimations.stream()
                        .mapToInt(QuotaAwareAnimationSet::getRunningAnimationCount)
                        .sum();
    }

    /** Returns the cost of nodes existing in the pipeline. */
    @VisibleForTesting
    public int getDynamicExpressionsNodesCost() {
        return mPositionIdTree.getAllNodes().stream()
                .mapToInt(NodeInfo::getExpressionDynamicNodesCost)
                .sum();
    }

    /** Returns whether all quota has been released. */
    @VisibleForTesting
    public boolean isAllQuotaReleased() {
        return mAnimationQuotaManager instanceof FixedQuotaManagerImpl
                && ((FixedQuotaManagerImpl) mAnimationQuotaManager).isAllQuotaReleased();
    }
}