Int32Nodes.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.protolayout.expression.pipeline;

import static java.lang.Math.abs;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
import androidx.wear.protolayout.expression.DynamicDataKey;
import androidx.wear.protolayout.expression.PlatformHealthSources;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
import androidx.wear.protolayout.expression.proto.DynamicDataProto.DynamicDataValue;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticInt32Op;
import androidx.wear.protolayout.expression.proto.DynamicProto.DurationPartType;
import androidx.wear.protolayout.expression.proto.DynamicProto.FloatToInt32Op;
import androidx.wear.protolayout.expression.proto.DynamicProto.GetDurationPartOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.GetZonedDateTimePartOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32Source;
import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32SourceType;
import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source;
import androidx.wear.protolayout.expression.proto.DynamicProto.ZonedDateTimePartType;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.function.Function;

/** Dynamic data nodes which yield integers. */
class Int32Nodes {

    private Int32Nodes() {}

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

        FixedInt32Node(
                FixedInt32 protoNode, DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            this.mValue = protoNode.getValue();
            this.mDownstream = downstream;
        }

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

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

        @Override
        @UiThread
        public void destroy() {}
    }

    /** Dynamic integer node that gets value from the platform source. */
    static class LegacyPlatformInt32SourceNode extends StateSourceNode<Integer> {

        LegacyPlatformInt32SourceNode(
                PlatformDataStore dataStore,
                PlatformInt32Source protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            super(
                    dataStore,
                    getDataKey(protoNode.getSourceType()),
                    getStateExtractor(protoNode.getSourceType()),
                    downstream);
        }

        @NonNull
        private static DynamicDataKey<?> getDataKey(PlatformInt32SourceType type) {
            if (type == PlatformInt32SourceType.PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE) {
                return PlatformHealthSources.Keys.HEART_RATE_BPM;
            }

            if (type == PlatformInt32SourceType.PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT) {
                return PlatformHealthSources.Keys.DAILY_STEPS;
            }

            throw new IllegalArgumentException(
                    "Unknown DynamicInt32 platform source type: " + type);
        }

        @NonNull
        private static Function<DynamicDataValue, Integer> getStateExtractor(
                PlatformInt32SourceType type) {
            if (type == PlatformInt32SourceType.PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE) {
                return se -> (int) se.getFloatVal().getValue();
            }

            if (type == PlatformInt32SourceType.PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT) {
                return se -> se.getInt32Val().getValue();
            }

            throw new IllegalArgumentException(
                    "Unknown DynamicInt32 platform source type: " + type);
        }
    }

    /** Dynamic integer node that supports arithmetic operations. */
    static class ArithmeticInt32Node extends DynamicDataBiTransformNode<Integer, Integer, Integer> {
        private static final String TAG = "ArithmeticInt32Node";

        ArithmeticInt32Node(
                ArithmeticInt32Op protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            super(
                    downstream,
                    (lhs, rhs) -> {
                        try {
                            switch (protoNode.getOperationType()) {
                                case ARITHMETIC_OP_TYPE_ADD:
                                    return lhs + rhs;
                                case ARITHMETIC_OP_TYPE_SUBTRACT:
                                    return lhs - rhs;
                                case ARITHMETIC_OP_TYPE_MULTIPLY:
                                    return lhs * rhs;
                                case ARITHMETIC_OP_TYPE_DIVIDE:
                                    return lhs / rhs;
                                case ARITHMETIC_OP_TYPE_MODULO:
                                    return lhs % rhs;
                                case ARITHMETIC_OP_TYPE_UNDEFINED:
                                case UNRECOGNIZED:
                                    break;
                            }
                        } catch (ArithmeticException ex) {
                            Log.e(TAG, "ArithmeticException in ArithmeticInt32Node", ex);
                            return null;
                        }

                        throw new IllegalArgumentException(
                                "Unknown operation type in ArithmeticInt32Node: "
                                        + protoNode.getOperationType());
                    });
        }
    }

    /** Dynamic integer node that gets value from the state. */
    static class StateInt32SourceNode extends StateSourceNode<Integer> {

        StateInt32SourceNode(
                DataStore dataStore,
                StateInt32Source protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            super(
                    dataStore,
                    StateSourceNode.<DynamicInt32>createKey(
                            protoNode.getSourceNamespace(), protoNode.getSourceKey()),
                    se -> se.getInt32Val().getValue(),
                    downstream);
        }
    }

    /** Dynamic integer node that gets value from float. */
    static class FloatToInt32Node extends DynamicDataTransformNode<Float, Integer> {

        FloatToInt32Node(
                FloatToInt32Op protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            super(
                    downstream,
                    x -> {
                        switch (protoNode.getRoundMode()) {
                            case ROUND_MODE_UNDEFINED:
                                // Round mode defaults to floor.
                            case ROUND_MODE_FLOOR:
                                return (int) Math.floor(x);
                            case ROUND_MODE_ROUND:
                                return Math.round(x);
                            case ROUND_MODE_CEILING:
                                return (int) Math.ceil(x);
                            case UNRECOGNIZED:
                                break;
                        }
                        throw new IllegalArgumentException(
                                "Unknown rounding mode:" + protoNode.getRoundMode());
                    },
                    x -> x - 1 < Integer.MAX_VALUE && x >= Integer.MIN_VALUE);
        }
    }

    /** Dynamic integer node that gets duration part from a duration. */
    static class GetDurationPartOpNode extends DynamicDataTransformNode<Duration, Integer> {
        private static final String TAG = "GetDurationPartOpNode";

        GetDurationPartOpNode(
                GetDurationPartOp protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            super(
                    downstream,
                    duration -> (int) getDurationPart(duration, protoNode.getDurationPart()));
        }

        private static long getDurationPart(Duration duration, DurationPartType durationPartType) {
            switch (durationPartType) {
                case DURATION_PART_TYPE_DAYS:
                    return abs(duration.getSeconds() / (3600 * 24));
                case DURATION_PART_TYPE_HOURS:
                    return abs((duration.getSeconds() / 3600) % 24);
                case DURATION_PART_TYPE_MINUTES:
                    return abs((duration.getSeconds() / 60) % 60);
                case DURATION_PART_TYPE_SECONDS:
                    return abs(duration.getSeconds() % 60);
                case DURATION_PART_TYPE_TOTAL_DAYS:
                    return duration.toDays();
                case DURATION_PART_TYPE_TOTAL_HOURS:
                    return duration.toHours();
                case DURATION_PART_TYPE_TOTAL_MINUTES:
                    return duration.toMinutes();
                case DURATION_PART_TYPE_TOTAL_SECONDS:
                    return duration.getSeconds();
                case DURATION_PART_TYPE_UNDEFINED:
                case UNRECOGNIZED:
                    break;
            }
            throw new IllegalArgumentException("Unknown duration part: " + durationPartType);
        }
    }

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

        private final AnimatableFixedInt32 mProtoNode;
        private final DynamicTypeValueReceiverWithPreUpdate<Integer> mDownstream;
        private boolean mFirstUpdateFromAnimatorDone = false;

        AnimatableFixedInt32Node(
                AnimatableFixedInt32 protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream,
                QuotaManager quotaManager) {
            super(quotaManager, protoNode.getAnimationSpec());
            this.mProtoNode = protoNode;
            this.mDownstream = downstream;
            mQuotaAwareAnimator.addUpdateCallback(
                    animatedValue -> {
                        // The onPreUpdate has already been called once before the first update.
                        if (mFirstUpdateFromAnimatorDone) {
                            mDownstream.onPreUpdate();
                        }
                        mDownstream.onData((Integer) animatedValue);
                        mFirstUpdateFromAnimatorDone = true;
                    });
        }

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

        @Override
        @UiThread
        public void init() {
            mQuotaAwareAnimator.setIntValues(mProtoNode.getFromValue(), mProtoNode.getToValue());
            // For the first update from the animator with the above from & to values, the
            // onPreUpdate has already been called.
            mFirstUpdateFromAnimatorDone = false;
            startOrSkipAnimator();
        }

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

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

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

        @Nullable Integer mCurrentValue = null;
        int mPendingCalls = 0;
        private boolean mFirstUpdateFromAnimatorDone = false;

        // 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")
        DynamicAnimatedInt32Node(
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream,
                @NonNull AnimationSpec spec,
                QuotaManager quotaManager) {
            super(quotaManager, spec);
            this.mDownstream = downstream;
            mQuotaAwareAnimator.addUpdateCallback(
                    animatedValue -> {
                        if (mPendingCalls == 0) {
                            // The onPreUpdate has already been called once before the first update.
                            if (mFirstUpdateFromAnimatorDone) {
                                mDownstream.onPreUpdate();
                            }
                            mCurrentValue = (Integer) animatedValue;
                            mDownstream.onData(mCurrentValue);
                            mFirstUpdateFromAnimatorDone = true;
                        }
                    });
            this.mInputCallback =
                    new DynamicTypeValueReceiverWithPreUpdate<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);
                                    // For the first update from the animator with the above from &
                                    // to values, the onPreUpdate has already been called.
                                    mFirstUpdateFromAnimatorDone = false;
                                    startOrSkipAnimator();
                                }
                            }
                        }

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

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

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

    /** Dynamic integer node that gets date-time part from a zoned date-time. */
    static class GetZonedDateTimePartOpNode
            extends DynamicDataTransformNode<ZonedDateTime, Integer> {
        private static final String TAG = "GetZonedDateTimePartOpNode";

        GetZonedDateTimePartOpNode(
                GetZonedDateTimePartOp protoNode,
                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
            super(downstream, zdt -> (int) getZonedDateTimePart(zdt, protoNode.getPartType()));
        }

        private static long getZonedDateTimePart(ZonedDateTime zdt, ZonedDateTimePartType type) {
            switch (type) {
                case ZONED_DATE_TIME_PART_SECOND:
                    return zdt.getSecond();
                case ZONED_DATE_TIME_PART_MINUTE:
                    return zdt.getMinute();
                case ZONED_DATE_TIME_PART_HOUR_24H:
                    return zdt.getHour();
                case ZONED_DATE_TIME_PART_DAY_OF_WEEK:
                    return zdt.getDayOfWeek().getValue();
                case ZONED_DATE_TIME_PART_DAY_OF_MONTH:
                    return zdt.getDayOfMonth();
                case ZONED_DATE_TIME_PART_MONTH:
                    return zdt.getMonth().getValue();
                case ZONED_DATE_TIME_PART_YEAR:
                    return zdt.getYear();
                case ZONED_DATE_TIME_PART_UNDEFINED:
                case UNRECOGNIZED:
                    break;
            }
            throw new IllegalArgumentException("Unknown ZonedDateTime part: " + type);
        }
    }
}