Fingerprint.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;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Represents a virtually unique fingerprint for a proto message.
 *
 * <p>Note that this actually represents the way a message was built and not necessarily its
 * contents. In other words, 2 messages with the same contents may have different fingerprints if
 * their setters were called in a different order.
 *
 * <p>A value of -1 for {@code selfPropsValue} means the self part should be considered different
 * when compared with other instances of this class. A value of -1 for {@code childNodesValue} means
 * the children part should be considered different when compared with other instances of this
 * class.
 */
@RestrictTo(Scope.LIBRARY_GROUP)
public final class Fingerprint {
    private static final int DEFAULT_VALUE = 0;
    private static final int DISCARDED_VALUE = -1;
    private final int selfTypeValue;
    private int selfPropsValue;
    private int childNodesValue;
    private @Nullable List<Fingerprint> childNodes;

    public Fingerprint(int selfTypeValue) {
        this.selfTypeValue = selfTypeValue;
        this.selfPropsValue = DEFAULT_VALUE;
        this.childNodesValue = DEFAULT_VALUE;
        this.childNodes = null;
    }

    /**
     * Get the aggregate numeric fingerprint, representing the message itself as well as all its
     * child nodes. Returns -1 if the fingerprint is discarded.
     */
    public int aggregateValueAsInt() {
        if (selfPropsValue == DISCARDED_VALUE) {
            return DISCARDED_VALUE;
        }
        int aggregateValue = selfTypeValue;
        aggregateValue = (31 * aggregateValue) + selfPropsValue;
        aggregateValue = (31 * aggregateValue) + childNodesValue;
        return aggregateValue;
    }

    /** Get the numeric fingerprint for the message's type. */
    public int selfTypeValue() {
        return selfTypeValue;
    }

    /**
     * Get the numeric fingerprint for the message's properties only, excluding its type and child
     * nodes. Returns -1 if the fingerprint is discarded.
     */
    public int selfPropsValue() {
        return selfPropsValue;
    }

    /**
     * Get the numeric fingerprint for the child nodes. Returns -1 if the fingerprint for children
     * is discarded.
     *
     * <p>Note: If {@link #childNodes()} is empty, the children should be considered fully discarded
     * at this level. Otherwise, at least one of the children is discarded (self discard) and the
     * fingerprint of each children should be checked individually.
     */
    public int childNodesValue() {
        return childNodesValue;
    }

    /**
     * Get the child nodes. Returns empty list if the node has no children, or if the child
     * fingerprints are discarded.
     */
    public @NonNull List<Fingerprint> childNodes() {
        return childNodes == null ? Collections.emptyList() : childNodes;
    }

    /** Add a child node to this fingerprint. */
    public void addChildNode(@NonNull Fingerprint childNode) {
        // Even if the children are not discarded directly through discardValued(true), if one of
        // them is individually discarded, we need to propagate that so that the differ knows it
        // has to go down one more level. That's why childNodesValue == DISCARDED_VALUE doesn't
        // necessarily mean all of the children are discarded. childNodes is used to
        // differentiate these two cases.
        if (selfPropsValue == DISCARDED_VALUE
                && childNodesValue == DISCARDED_VALUE
                && childNodes == null) {
            return;
        }
        if (childNodes == null) {
            childNodes = new ArrayList<>();
        }
        childNodes.add(childNode);
        if (childNode.selfPropsValue == DISCARDED_VALUE) {
            childNodesValue = DISCARDED_VALUE;
        } else if (childNodesValue != DISCARDED_VALUE) {
            childNodesValue = (31 * childNodesValue) + childNode.aggregateValueAsInt();
        }
    }

    /**
     * Discard values of this fingerprint.
     *
     * @param includeChildren if True, discards children values of this fingerprints too.
     */
    public void discardValues(boolean includeChildren) {
        if (selfPropsValue == DISCARDED_VALUE
                && childNodesValue == DISCARDED_VALUE
                && !includeChildren) {
            throw new IllegalStateException(
                    "Container is in discarded state. Children can't be reinstated.");
        }
        selfPropsValue = DISCARDED_VALUE;
        if (includeChildren) {
            childNodesValue = DISCARDED_VALUE;
            childNodes = null;
        }
    }

    /** Record a property value being updated. */
    public void recordPropertyUpdate(int fieldNumber, int valueHash) {
        recordEntry(fieldNumber);
        recordEntry(valueHash);
    }

    private void recordEntry(int entry) {
        selfPropsValue = (31 * selfPropsValue) + entry;
    }
}