PrimaryLayout.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.tiles.material.layouts;

import static androidx.annotation.Dimension.DP;
import static androidx.wear.tiles.material.ChipDefaults.COMPACT_HEIGHT_TAPPABLE;
import static androidx.wear.tiles.material.Helper.checkNotNull;
import static androidx.wear.tiles.material.Helper.checkTag;
import static androidx.wear.tiles.material.Helper.getMetadataTagBytes;
import static androidx.wear.tiles.material.Helper.getTagBytes;
import static androidx.wear.tiles.material.Helper.isRoundDevice;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.DEFAULT_VERTICAL_SPACER_HEIGHT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP;

import android.annotation.SuppressLint;

import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.expression.Fingerprint;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.tiles.material.CompactChip;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;

/**
 * Tiles layout that represents a suggested layout style for Material Tiles with the primary
 * (compact) chip at the bottom with the given content in the center and the recommended margin and
 * padding applied. There is a fixed slot for an optional primary label above or optional secondary
 * label below the main content area.
 *
 * <p>It is highly recommended that main content has max lines between 2 and 4 (dependant on labels
 * present), i.e.: * No labels are present: content with max 4 lines, * 1 label is present: content
 * with max 3 lines, * 2 labels are present: content with max 2 lines.
 *
 * <p>For additional examples and suggested layouts see <a
 * href="/training/wearables/design/tiles-design-system">Tiles Design System</a>.
 *
 * <p>When accessing the contents of a container for testing, note that this element can't be simply
 * casted back to the original type, i.e.:
 *
 * <pre>{@code
 * PrimaryLayout pl = new PrimaryLayout...
 * Box box = new Box.Builder().addContent(pl).build();
 *
 * PrimaryLayout myPl = (PrimaryLayout) box.getContents().get(0);
 * }</pre>
 *
 * will fail.
 *
 * <p>To be able to get {@link PrimaryLayout} object from any layout element, {@link
 * #fromLayoutElement} method should be used, i.e.:
 *
 * <pre>{@code
 * PrimaryLayout myPl = PrimaryLayout.fromLayoutElement(box.getContents().get(0));
 * }</pre>
 *
 * @deprecated Use the new class {@link androidx.wear.protolayout.material.layouts.PrimaryLayout}
 *     which provides the same API and functionality.
 */
@Deprecated
@SuppressWarnings("deprecation")
public class PrimaryLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
    /**
     * Prefix tool tag for Metadata in androidx.wear.tiles.ModifiersBuilders.Modifiers, so we know
     * that androidx.wear.tiles.LayoutElementBuilders.Box is actually a PrimaryLayout.
     */
    static final String METADATA_TAG_PREFIX = "PL_";

    /** Index for byte array that contains bits to check whether the contents are present or not. */
    static final int FLAG_INDEX = METADATA_TAG_PREFIX.length();

    /**
     * Base tool tag for Metadata in androidx.wear.tiles.ModifiersBuilders.Modifiers, so we know
     * that androidx.wear.tiles.LayoutElementBuilders.Box is actually a PrimaryLayout and what
     * optional content is added.
     */
    static final byte[] METADATA_TAG_BASE =
            Arrays.copyOf(getTagBytes(METADATA_TAG_PREFIX), FLAG_INDEX + 1);

    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the primary chip is present or not.
     */
    static final int CHIP_PRESENT = 0x1;
    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the primary label is present or not.
     */
    static final int PRIMARY_LABEL_PRESENT = 0x2;
    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the secondary label is present or not.
     */
    static final int SECONDARY_LABEL_PRESENT = 0x4;
    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the content is present or not.
     */
    static final int CONTENT_PRESENT = 0x8;

    /** Position of the primary label in its own inner column if exists. */
    static final int PRIMARY_LABEL_POSITION = 1;
    /** Position of the content in its own inner column. */
    static final int CONTENT_ONLY_POSITION = 0;
    /** Position of the primary chip in main layout column. */
    static final int PRIMARY_CHIP_POSITION = 1;

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            flag = true,
            value = {CHIP_PRESENT, PRIMARY_LABEL_PRESENT, SECONDARY_LABEL_PRESENT, CONTENT_PRESENT})
    @interface ContentBits {}

    @NonNull private final androidx.wear.tiles.LayoutElementBuilders.Box mImpl;

    // This contains inner columns and primary chip.
    @NonNull
    private final List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement> mAllContent;
    // This contains optional labels, spacers and main content.
    @NonNull
    private final List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement> mPrimaryLabel;
    // This contains optional labels, spacers and main content.
    @NonNull
    private final List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement>
            mContentAndSecondaryLabel;

    PrimaryLayout(@NonNull androidx.wear.tiles.LayoutElementBuilders.Box layoutElement) {
        this.mImpl = layoutElement;
        this.mAllContent =
                ((androidx.wear.tiles.LayoutElementBuilders.Column)
                                layoutElement.getContents().get(0))
                        .getContents();
        List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement> innerContent =
                ((androidx.wear.tiles.LayoutElementBuilders.Column) mAllContent.get(0))
                        .getContents();
        this.mPrimaryLabel =
                ((androidx.wear.tiles.LayoutElementBuilders.Column) innerContent.get(0))
                        .getContents();
        this.mContentAndSecondaryLabel =
                ((androidx.wear.tiles.LayoutElementBuilders.Column)
                                ((androidx.wear.tiles.LayoutElementBuilders.Box)
                                                innerContent.get(1))
                                        .getContents()
                                        .get(0))
                        .getContents();
    }

    /** Builder class for {@link PrimaryLayout}. */
    public static final class Builder
            implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
        @NonNull
        private final androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters
                mDeviceParameters;

        @Nullable
        private androidx.wear.tiles.LayoutElementBuilders.LayoutElement mPrimaryChip = null;

        @Nullable
        private androidx.wear.tiles.LayoutElementBuilders.LayoutElement mPrimaryLabelText = null;

        @Nullable
        private androidx.wear.tiles.LayoutElementBuilders.LayoutElement mSecondaryLabelText = null;

        @NonNull
        private androidx.wear.tiles.LayoutElementBuilders.LayoutElement mContent =
                new androidx.wear.tiles.LayoutElementBuilders.Box.Builder().build();

        @NonNull
        private androidx.wear.tiles.DimensionBuilders.DpProp mVerticalSpacerHeight =
                DEFAULT_VERTICAL_SPACER_HEIGHT;

        private byte mMetadataContentByte = 0;

        /**
         * Creates a builder for the {@link PrimaryLayout} from the given content. Content inside of
         * it can later be set with {@link #setContent}, {@link #setPrimaryChipContent}, {@link
         * #setPrimaryLabelTextContent} and {@link #setSecondaryLabelTextContent}.
         */
        public Builder(
                @NonNull
                        androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters
                                deviceParameters) {
            this.mDeviceParameters = deviceParameters;
        }

        /**
         * Sets the element which is in the slot at the bottom of the layout. Note that it is
         * accepted to pass in any {@link androidx.wear.tiles.LayoutElementBuilders.LayoutElement},
         * but it is strongly recommended to add a {@link CompactChip} as the layout is optimized
         * for it.
         */
        @NonNull
        public Builder setPrimaryChipContent(
                @NonNull androidx.wear.tiles.LayoutElementBuilders.LayoutElement compactChip) {
            this.mPrimaryChip = compactChip;
            mMetadataContentByte = (byte) (mMetadataContentByte | CHIP_PRESENT);
            return this;
        }

        /** Sets the content in the primary label slot which will be above the main content. */
        @NonNull
        public Builder setPrimaryLabelTextContent(
                @NonNull androidx.wear.tiles.LayoutElementBuilders.LayoutElement primaryLabelText) {
            this.mPrimaryLabelText = primaryLabelText;
            mMetadataContentByte = (byte) (mMetadataContentByte | PRIMARY_LABEL_PRESENT);
            return this;
        }

        /**
         * Sets the content in the primary label slot which will be below the main content. It is
         * highly recommended to have primary label set when having secondary label.
         */
        @NonNull
        public Builder setSecondaryLabelTextContent(
                @NonNull
                        androidx.wear.tiles.LayoutElementBuilders.LayoutElement
                                secondaryLabelText) {
            this.mSecondaryLabelText = secondaryLabelText;
            mMetadataContentByte = (byte) (mMetadataContentByte | SECONDARY_LABEL_PRESENT);
            return this;
        }

        /**
         * Sets the additional content to this layout, above the primary chip.
         *
         * <p>The content slot will wrap the elements' height, so the height of the given content
         * must be fixed or set to wrap ({@code expand} can't be used).
         *
         * <p>This layout has built-in horizontal margins, so the given content should have width
         * set to {@code expand} to use all the available space, rather than an explicit width which
         * may lead to clipping.
         */
        @NonNull
        public Builder setContent(
                @NonNull androidx.wear.tiles.LayoutElementBuilders.LayoutElement content) {
            this.mContent = content;
            mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_PRESENT);
            return this;
        }

        /**
         * Sets the vertical spacer height which is used as a space between main content and
         * secondary label if there is any. If not set, {@link
         * LayoutDefaults#DEFAULT_VERTICAL_SPACER_HEIGHT} will be used.
         */
        @NonNull
        // The @Dimension(unit = DP) on dp() is seemingly being ignored, so lint complains that
        // we're passing PX to something expecting DP. Just suppress the warning for now.
        @SuppressLint("ResourceType")
        public Builder setVerticalSpacerHeight(@Dimension(unit = DP) float height) {
            this.mVerticalSpacerHeight = androidx.wear.tiles.DimensionBuilders.dp(height);
            return this;
        }

        /** Constructs and returns {@link PrimaryLayout} with the provided content and look. */
        // The @Dimension(unit = DP) on dp() is seemingly being ignored, so lint complains that
        // we're passing DP to something expecting PX. Just suppress the warning for now.
        @SuppressLint("ResourceType")
        @NonNull
        @Override
        public PrimaryLayout build() {
            float topPadding = getTopPadding();
            float bottomPadding = getBottomPadding();
            float horizontalPadding = getHorizontalPadding();
            float horizontalChipPadding = getChipHorizontalPadding();

            float primaryChipHeight = mPrimaryChip != null ? COMPACT_HEIGHT_TAPPABLE.getValue() : 0;

            androidx.wear.tiles.DimensionBuilders.DpProp mainContentHeight =
                    androidx.wear.tiles.DimensionBuilders.dp(
                            mDeviceParameters.getScreenHeightDp()
                                    - primaryChipHeight
                                    - bottomPadding
                                    - topPadding);

            // Layout organization: column(column(primary label + spacer + (box(column(content +
            // secondary label))) + chip)

            // First column that has all other content and chip.
            androidx.wear.tiles.LayoutElementBuilders.Column.Builder layoutBuilder =
                    new androidx.wear.tiles.LayoutElementBuilders.Column.Builder();

            // Contains primary label, main content and secondary label. Primary label will be
            // wrapped, while other content will be expanded so it can be centered in the remaining
            // space.
            androidx.wear.tiles.LayoutElementBuilders.Column.Builder contentAreaBuilder =
                    new androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
                            .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                            .setHeight(mainContentHeight)
                            .setHorizontalAlignment(
                                    androidx.wear.tiles.LayoutElementBuilders
                                            .HORIZONTAL_ALIGN_CENTER);

            // Contains main content and secondary label with wrapped height so it can be put inside
            // of the androidx.wear.tiles.LayoutElementBuilders.Box to be centered.
            androidx.wear.tiles.LayoutElementBuilders.Column.Builder contentSecondaryLabelBuilder =
                    new androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
                            .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                            .setHeight(androidx.wear.tiles.DimensionBuilders.wrap())
                            .setHorizontalAlignment(
                                    androidx.wear.tiles.LayoutElementBuilders
                                            .HORIZONTAL_ALIGN_CENTER);

            // Needs to be in column because of the spacers.
            androidx.wear.tiles.LayoutElementBuilders.Column.Builder primaryLabelBuilder =
                    new androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
                            .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                            .setHeight(androidx.wear.tiles.DimensionBuilders.wrap());

            if (mPrimaryLabelText != null) {
                primaryLabelBuilder.addContent(
                        new androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder()
                                .setHeight(getPrimaryLabelTopSpacerHeight())
                                .build());
                primaryLabelBuilder.addContent(mPrimaryLabelText);
            }

            contentAreaBuilder.addContent(primaryLabelBuilder.build());

            contentSecondaryLabelBuilder.addContent(
                    new androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
                            .setVerticalAlignment(
                                    androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
                            .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                            .setHeight(androidx.wear.tiles.DimensionBuilders.wrap())
                            .addContent(mContent)
                            .build());

            if (mSecondaryLabelText != null) {
                contentSecondaryLabelBuilder.addContent(
                        new androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder()
                                .setHeight(mVerticalSpacerHeight)
                                .build());
                contentSecondaryLabelBuilder.addContent(mSecondaryLabelText);
            }

            contentAreaBuilder.addContent(
                    new androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
                            .setVerticalAlignment(
                                    androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
                            .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                            .setHeight(androidx.wear.tiles.DimensionBuilders.expand())
                            .addContent(contentSecondaryLabelBuilder.build())
                            .build());

            layoutBuilder
                    .setModifiers(
                            new androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder()
                                    .setPadding(
                                            new androidx.wear.tiles.ModifiersBuilders.Padding
                                                            .Builder()
                                                    .setStart(
                                                            androidx.wear.tiles.DimensionBuilders
                                                                    .dp(horizontalPadding))
                                                    .setEnd(
                                                            androidx.wear.tiles.DimensionBuilders
                                                                    .dp(horizontalPadding))
                                                    .setTop(
                                                            androidx.wear.tiles.DimensionBuilders
                                                                    .dp(topPadding))
                                                    .setBottom(
                                                            androidx.wear.tiles.DimensionBuilders
                                                                    .dp(bottomPadding))
                                                    .build())
                                    .build())
                    .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                    .setHeight(androidx.wear.tiles.DimensionBuilders.expand())
                    .setHorizontalAlignment(
                            androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER);

            layoutBuilder.addContent(contentAreaBuilder.build());

            if (mPrimaryChip != null) {
                layoutBuilder.addContent(
                        new androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
                                .setVerticalAlignment(
                                        androidx.wear.tiles.LayoutElementBuilders
                                                .VERTICAL_ALIGN_BOTTOM)
                                .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                                .setHeight(androidx.wear.tiles.DimensionBuilders.wrap())
                                .setModifiers(
                                        new androidx.wear.tiles.ModifiersBuilders.Modifiers
                                                        .Builder()
                                                .setPadding(
                                                        new androidx.wear.tiles.ModifiersBuilders
                                                                        .Padding.Builder()
                                                                .setStart(
                                                                        androidx.wear.tiles
                                                                                .DimensionBuilders
                                                                                .dp(
                                                                                        horizontalChipPadding))
                                                                .setEnd(
                                                                        androidx.wear.tiles
                                                                                .DimensionBuilders
                                                                                .dp(
                                                                                        horizontalChipPadding))
                                                                .build())
                                                .build())
                                .addContent(mPrimaryChip)
                                .build());
            }

            byte[] metadata = METADATA_TAG_BASE.clone();
            metadata[FLAG_INDEX] = mMetadataContentByte;

            androidx.wear.tiles.LayoutElementBuilders.Box.Builder element =
                    new androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
                            .setWidth(androidx.wear.tiles.DimensionBuilders.expand())
                            .setHeight(androidx.wear.tiles.DimensionBuilders.expand())
                            .setModifiers(
                                    new androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder()
                                            .setMetadata(
                                                    new androidx.wear.tiles.ModifiersBuilders
                                                                    .ElementMetadata.Builder()
                                                            .setTagData(metadata)
                                                            .build())
                                            .build())
                            .setVerticalAlignment(
                                    androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
                            .addContent(layoutBuilder.build());

            return new PrimaryLayout(element.build());
        }

        /**
         * Returns the recommended bottom padding, based on percentage values in {@link
         * LayoutDefaults}.
         */
        private float getBottomPadding() {
            return mPrimaryChip != null
                    ? (mDeviceParameters.getScreenHeightDp()
                            * (isRoundDevice(mDeviceParameters)
                                    ? PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT
                                    : PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT))
                    : getTopPadding();
        }

        /**
         * Returns the recommended top padding, based on percentage values in {@link
         * LayoutDefaults}.
         */
        @Dimension(unit = DP)
        private float getTopPadding() {
            return mDeviceParameters.getScreenHeightDp()
                    * (isRoundDevice(mDeviceParameters)
                            ? PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT
                            : PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT);
        }

        /**
         * Returns the recommended horizontal padding, based on percentage values in {@link
         * LayoutDefaults}.
         */
        @Dimension(unit = DP)
        private float getHorizontalPadding() {
            return mDeviceParameters.getScreenWidthDp()
                    * (isRoundDevice(mDeviceParameters)
                            ? PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT
                            : PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT);
        }

        /**
         * Returns the recommended horizontal padding for primary chip, based on percentage values
         * and DP values in {@link LayoutDefaults}.
         */
        @Dimension(unit = DP)
        private float getChipHorizontalPadding() {
            return isRoundDevice(mDeviceParameters)
                    ? PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP
                    : PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP;
        }

        /** Returns the spacer height to be placed above primary label to accommodate Tile icon. */
        @NonNull
        private androidx.wear.tiles.DimensionBuilders.DpProp getPrimaryLabelTopSpacerHeight() {
            return isRoundDevice(mDeviceParameters)
                    ? PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP
                    : PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP;
        }
    }

    /** Get the primary label content from this layout. */
    @Nullable
    public androidx.wear.tiles.LayoutElementBuilders.LayoutElement getPrimaryLabelTextContent() {
        if (!areElementsPresent(PRIMARY_LABEL_PRESENT)) {
            return null;
        }
        return mPrimaryLabel.get(PRIMARY_LABEL_POSITION);
    }

    /** Get the secondary label content from this layout. */
    @Nullable
    public androidx.wear.tiles.LayoutElementBuilders.LayoutElement getSecondaryLabelTextContent() {
        if (!areElementsPresent(SECONDARY_LABEL_PRESENT)) {
            return null;
        }
        // By tag we know that secondary label exists. It will always be at last position.
        return mContentAndSecondaryLabel.get(mContentAndSecondaryLabel.size() - 1);
    }

    /** Get the inner content from this layout. */
    @Nullable
    public androidx.wear.tiles.LayoutElementBuilders.LayoutElement getContent() {
        if (!areElementsPresent(CONTENT_PRESENT)) {
            return null;
        }
        return ((androidx.wear.tiles.LayoutElementBuilders.Box)
                        mContentAndSecondaryLabel.get(CONTENT_ONLY_POSITION))
                .getContents()
                .get(0);
    }

    /** Get the primary chip content from this layout. */
    @Nullable
    public androidx.wear.tiles.LayoutElementBuilders.LayoutElement getPrimaryChipContent() {
        if (areElementsPresent(CHIP_PRESENT)) {
            return ((androidx.wear.tiles.LayoutElementBuilders.Box)
                            mAllContent.get(PRIMARY_CHIP_POSITION))
                    .getContents()
                    .get(0);
        }
        return null;
    }

    /** Get the vertical spacer height from this layout. */
    // The @Dimension(unit = DP) on getValue() is seemingly being ignored, so lint complains that
    // we're passing PX to something expecting DP. Just suppress the warning for now.
    @SuppressLint("ResourceType")
    @Dimension(unit = DP)
    public float getVerticalSpacerHeight() {
        if (areElementsPresent(SECONDARY_LABEL_PRESENT)) {
            androidx.wear.tiles.LayoutElementBuilders.LayoutElement element =
                    mContentAndSecondaryLabel.get(CONTENT_ONLY_POSITION + 1);
            if (element instanceof androidx.wear.tiles.LayoutElementBuilders.Spacer) {
                androidx.wear.tiles.DimensionBuilders.SpacerDimension height =
                        ((androidx.wear.tiles.LayoutElementBuilders.Spacer) element).getHeight();
                if (height instanceof androidx.wear.tiles.DimensionBuilders.DpProp) {
                    return ((androidx.wear.tiles.DimensionBuilders.DpProp) height).getValue();
                }
            }
        }
        return DEFAULT_VERTICAL_SPACER_HEIGHT.getValue();
    }

    private boolean areElementsPresent(@ContentBits int elementFlag) {
        return (getMetadataTag()[FLAG_INDEX] & elementFlag) == elementFlag;
    }

    /** Returns metadata tag set to this PrimaryLayout. */
    @NonNull
    byte[] getMetadataTag() {
        return getMetadataTagBytes(checkNotNull(checkNotNull(mImpl.getModifiers()).getMetadata()));
    }

    /**
     * Returns PrimaryLayout object from the given
     * androidx.wear.tiles.LayoutElementBuilders.LayoutElement (e.g. one retrieved from a
     * container's content with {@code container.getContents().get(index)}) if that element can be
     * converted to PrimaryLayout. Otherwise, it will return null.
     */
    @Nullable
    public static PrimaryLayout fromLayoutElement(
            @NonNull androidx.wear.tiles.LayoutElementBuilders.LayoutElement element) {
        if (element instanceof PrimaryLayout) {
            return (PrimaryLayout) element;
        }
        if (!(element instanceof androidx.wear.tiles.LayoutElementBuilders.Box)) {
            return null;
        }
        androidx.wear.tiles.LayoutElementBuilders.Box boxElement =
                (androidx.wear.tiles.LayoutElementBuilders.Box) element;
        if (!checkTag(boxElement.getModifiers(), METADATA_TAG_PREFIX, METADATA_TAG_BASE)) {
            return null;
        }
        // Now we are sure that this element is a PrimaryLayout.
        return new PrimaryLayout(boxElement);
    }

    @NonNull
    @Override
    @RestrictTo(Scope.LIBRARY_GROUP)
    public LayoutElementProto.LayoutElement toLayoutElementProto() {
        return mImpl.toLayoutElementProto();
    }

    @RestrictTo(Scope.LIBRARY_GROUP)
    @Nullable
    @Override
    public Fingerprint getFingerprint() {
        return mImpl.getFingerprint();
    }
}