DynamicDataBiTransformNode.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.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * Dynamic data node that can perform a transformation from two upstream nodes. This should be
 * created by passing a {@link Function} in, which implements the transformation.
 *
 * <p>The two inputs to this are called the left/right-hand side of the operation, since many of the
 * operations extending this class are likely to be simple maths operations. Conventionally then,
 * descendants of this class will implement operations of the form "O = LHS [op] RHS", or "O =
 * op(LHS, RHS)".
 *
 * <p>This node will wait until there's no pending data from either side:
 *
 * <ul>
 *   <li>This node will wait until both sides invoked either {@code onData} or {@code onInvalidated}
 *       at least once.
 *   <li>If one side invoked {@code onPreUpdate}, this node will wait until it invoked either {@code
 *       onData} or {@code onInvalidated}.
 *   <li>This node expects each side to invoke exactly on {@code onData} or {@code onInvalidated}
 *       after each {@code onPreUpdate}:
 *       <ul>
 *         <li>If {@code onData} or {@code onInvalidated} are invoked without {@code onPreUpdate},
 *             this node will log a warning and downstream the callback immediately (unless waiting
 *             for the other side).
 *         <li>If {@code onPreUpdate} is invoked more than once without {@code onData} or {@code
 *             onInvalidated}, this node will log a warning and ignore the followup {@code
 *             onPreUpdate}.
 *       </ul>
 *       Both of these scenarios can mean {@link DynamicTypeEvaluator} might publish a partially
 *       evaluated update.
 * </ul>
 *
 * @param <LhsT> The source data type for the left-hand side of the operation.
 * @param <RhsT> The source data type for the right-hand side of the operation.
 * @param <O> The data type that this node emits.
 */
class DynamicDataBiTransformNode<LhsT, RhsT, O> implements DynamicDataNode<O> {
    private static final String TAG = "DynamicDataBiTransform";

    private final UpstreamCallback<LhsT> mLhsUpstreamCallback = new UpstreamCallback<>();
    private final UpstreamCallback<RhsT> mRhsUpstreamCallback = new UpstreamCallback<>();

    final DynamicTypeValueReceiverWithPreUpdate<O> mDownstream;
    private final BiFunction<LhsT, RhsT, O> mTransformer;

    boolean mDownstreamPreUpdated = false;

    DynamicDataBiTransformNode(
            DynamicTypeValueReceiverWithPreUpdate<O> downstream,
            BiFunction<LhsT, RhsT, O> transformer) {
        this.mDownstream = downstream;
        this.mTransformer = transformer;
    }

    private class UpstreamCallback<T> implements DynamicTypeValueReceiverWithPreUpdate<T> {
        private boolean mUpstreamPreUpdated = false;

        /**
         * Whether {@link #cache} should be used, i.e. set to true when either {@link #onData} or
         * {@link #onInvalidated} were invoked at least once, and there's no pending {@link
         * #onPreUpdate} call.
         */
        boolean isCacheReady = false;

        /**
         * Latest value arrived in {@link #onData}, or {@code null} if the last callback was {@link
         * #onInvalidated}. Should not be used if {@link #isCacheReady} is {@code false}.
         */
        @Nullable T cache;

        @Override
        public void onPreUpdate() {
            if (mUpstreamPreUpdated) {
                Log.w(TAG, "Received onPreUpdate twice without onData/onInvalidated.");
            }
            isCacheReady = false;
            mUpstreamPreUpdated = true;
            if (!mDownstreamPreUpdated) {
                mDownstreamPreUpdated = true;
                mDownstream.onPreUpdate();
            }
        }

        @Override
        public void onData(@NonNull T newData) {
            onUpdate(newData);
        }

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

        private void onUpdate(@Nullable T newData) {
            if (!mUpstreamPreUpdated) {
                Log.w(TAG, "Received onData/onInvalidated without onPreUpdate.");
            }
            mUpstreamPreUpdated = false;
            isCacheReady = true;
            cache = newData;
            handleStateUpdate();
        }
    }

    void handleStateUpdate() {
        if (!mLhsUpstreamCallback.isCacheReady || !mRhsUpstreamCallback.isCacheReady) {
            return;
        }
        LhsT lhs = mLhsUpstreamCallback.cache;
        RhsT rhs = mRhsUpstreamCallback.cache;

        if (lhs == null || rhs == null) {
            mDownstream.onInvalidated();
        } else {
            O result = mTransformer.apply(lhs, rhs);
            if (result == null) {
                mDownstream.onInvalidated();
            } else {
                mDownstream.onData(result);
            }
        }
    }

    public DynamicTypeValueReceiverWithPreUpdate<LhsT> getLhsUpstreamCallback() {
        return mLhsUpstreamCallback;
    }

    public DynamicTypeValueReceiverWithPreUpdate<RhsT> getRhsUpstreamCallback() {
        return mRhsUpstreamCallback;
    }
}