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;
  }
}