ColorNodes.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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedColor;
import androidx.wear.protolayout.expression.proto.DynamicProto.StateColorSource;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;

/** Dynamic data nodes which yield colors. */
class ColorNodes {
    private ColorNodes() {}

    /** Dynamic color node that has a fixed value. */
    static class FixedColorNode implements DynamicDataSourceNode<Integer> {
        private final int mValue;
        private final DynamicTypeValueReceiver<Integer> mDownstream;

        FixedColorNode(FixedColor protoNode, DynamicTypeValueReceiver<Integer> downstream) {
            this.mValue = protoNode.getArgb();
            this.mDownstream = downstream;
        }

        @Override
        @UiThread
        public void preInit() {
            mDownstream.onPreUpdate();
        }

        @Override
        @UiThread
        public void init() {
            mDownstream.onData(mValue);
        }

        @Override
        public void destroy() {}
    }

    /** Dynamic color node that gets value from the platform source. */
    static class StateColorSourceNode extends StateSourceNode<Integer> {
        StateColorSourceNode(
                StateStore stateStore,
                StateColorSource protoNode,
                DynamicTypeValueReceiver<Integer> downstream) {
            super(
                    stateStore,
                    protoNode.getSourceKey(),
                    se -> se.getColorVal().getArgb(),
                    downstream);
        }
    }

    /** Dynamic color node that gets animatable value from fixed source. */
    static class AnimatableFixedColorNode extends AnimatableNode
            implements DynamicDataSourceNode<Integer> {

        private final AnimatableFixedColor mProtoNode;
        private final DynamicTypeValueReceiver<Integer> mDownstream;

        AnimatableFixedColorNode(
                AnimatableFixedColor protoNode,
                DynamicTypeValueReceiver<Integer> downstream,
                QuotaManager quotaManager) {

            super(quotaManager, protoNode.getAnimationSpec(), ARGB_EVALUATOR);
            this.mProtoNode = protoNode;
            this.mDownstream = downstream;
            mQuotaAwareAnimator.addUpdateCallback(
                    animatedValue -> mDownstream.onData((Integer) animatedValue));
        }

        @Override
        @UiThread
        public void preInit() {
            mDownstream.onPreUpdate();
        }

        @Override
        @UiThread
        public void init() {
            mQuotaAwareAnimator.setIntValues(mProtoNode.getFromArgb(), mProtoNode.getToArgb());
            startOrSkipAnimator();
        }

        @Override
        @UiThread
        public void destroy() {
            mQuotaAwareAnimator.stopAnimator();
        }
    }

    /** Dynamic color node that gets animatable value from dynamic source. */
    static class DynamicAnimatedColorNode extends AnimatableNode
            implements DynamicDataNode<Integer> {

        final DynamicTypeValueReceiver<Integer> mDownstream;
        private final DynamicTypeValueReceiver<Integer> mInputCallback;

        @Nullable Integer mCurrentValue = null;
        int mPendingCalls = 0;

        // Static analysis complains about calling methods of parent class AnimatableNode under
        // initialization but mInputCallback is only used after the constructor is finished.
        @SuppressWarnings("method.invocation.invalid")
        DynamicAnimatedColorNode(
                DynamicTypeValueReceiver<Integer> downstream,
                @NonNull AnimationSpec spec,
                QuotaManager quotaManager) {

            super(quotaManager, spec, ARGB_EVALUATOR);
            this.mDownstream = downstream;
            mQuotaAwareAnimator.addUpdateCallback(
                    animatedValue -> {
                        if (mPendingCalls == 0) {
                            mCurrentValue = (Integer) animatedValue;
                            mDownstream.onData(mCurrentValue);
                        }
                    });
            this.mInputCallback =
                    new DynamicTypeValueReceiver<Integer>() {
                        @Override
                        public void onPreUpdate() {
                            mPendingCalls++;

                            if (mPendingCalls == 1) {
                                mDownstream.onPreUpdate();
                            }
                        }

                        @Override
                        public void onData(@NonNull Integer newData) {
                            if (mPendingCalls > 0) {
                                mPendingCalls--;
                            }

                            if (mPendingCalls == 0) {
                                if (mCurrentValue == null) {
                                    mCurrentValue = newData;
                                    mDownstream.onData(mCurrentValue);
                                } else {
                                    mQuotaAwareAnimator.setIntValues(mCurrentValue, newData);
                                    startOrSkipAnimator();
                                }
                            }
                        }

                        @Override
                        public void onInvalidated() {
                            if (mPendingCalls > 0) {
                                mPendingCalls--;
                            }

                            if (mPendingCalls == 0) {
                                mCurrentValue = null;
                                mDownstream.onInvalidated();
                            }
                        }
                    };
        }

        public DynamicTypeValueReceiver<Integer> getInputCallback() {
            return mInputCallback;
        }
    }
}