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

import static androidx.annotation.Dimension.DP;
import static androidx.wear.protolayout.DimensionBuilders.dp;
import static androidx.wear.protolayout.DimensionBuilders.expand;
import static androidx.wear.protolayout.DimensionBuilders.wrap;
import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_PADDING;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_CONTENT_AND_SECONDARY_LABEL_SPACING_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_MARGIN_HORIZONTAL_ROUND_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_MARGIN_HORIZONTAL_SQUARE_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_PADDING_ABOVE_MAIN_CONTENT_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_PADDING_BELOW_MAIN_CONTENT_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_RESPONSIVE_MARGIN_HORIZONTAL_PERCENT;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_RESPONSIVE_MARGIN_VERTICAL_PERCENT;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_RESPONSIVE_OUTER_MARGIN_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.EDGE_CONTENT_LAYOUT_RESPONSIVE_PRIMARY_LABEL_SPACING_DP;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.LAYOUTS_LABEL_PADDING_PERCENT;
import static androidx.wear.protolayout.material.layouts.LayoutDefaults.insetElementWithPadding;
import static androidx.wear.protolayout.materialcore.Helper.checkNotNull;
import static androidx.wear.protolayout.materialcore.Helper.checkTag;
import static androidx.wear.protolayout.materialcore.Helper.getMetadataTagBytes;
import static androidx.wear.protolayout.materialcore.Helper.getTagBytes;
import static androidx.wear.protolayout.materialcore.Helper.isRoundDevice;

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.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.protolayout.DimensionBuilders.DpProp;
import androidx.wear.protolayout.DimensionBuilders.SpacerDimension;
import androidx.wear.protolayout.LayoutElementBuilders;
import androidx.wear.protolayout.LayoutElementBuilders.Box;
import androidx.wear.protolayout.LayoutElementBuilders.Column;
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
import androidx.wear.protolayout.LayoutElementBuilders.Spacer;
import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
import androidx.wear.protolayout.ModifiersBuilders.Padding;
import androidx.wear.protolayout.expression.Fingerprint;
import androidx.wear.protolayout.material.CircularProgressIndicator;
import androidx.wear.protolayout.proto.LayoutElementProto;

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

/**
 * ProtoLayout layout that represents the suggested layout style for Material ProtoLayout, which has
 * content around the edge of the screen (e.g. a ProgressIndicator) and the given content inside of
 * it with the recommended margin and padding applied. Optional primary or secondary label can be
 * added above and below the additional content, respectively.
 *
 * <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
 * EdgeContentLayout ecl = new EdgeContentLayout...
 * Box box = new Box.Builder().addContent(ecl).build();
 *
 * EdgeContentLayout myEcl = (EdgeContentLayout) box.getContents().get(0);
 * }</pre>
 *
 * will fail.
 *
 * <p>To be able to get {@link EdgeContentLayout} object from any layout element, {@link
 * #fromLayoutElement} method should be used, i.e.:
 *
 * <pre>{@code
 * EdgeContentLayout myEcl =
 *   EdgeContentLayout.fromLayoutElement(box.getContents().get(0));
 * }</pre>
 */
// TODO(b/274916652): Link visuals once they are available.
public class EdgeContentLayout implements LayoutElement {
    /**
     * Prefix tool tag for Metadata in Modifiers, so we know that Box is actually a
     * EdgeContentLayout.
     */
    static final String METADATA_TAG_PREFIX = "ECL_";

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

    /**
     * Base tool tag for Metadata in Modifiers, so we know that Box is actually a EdgeContentLayout
     * 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 edge content is present or not.
     */
    static final int EDGE_CONTENT_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 = 1 << 1;
    /**
     * 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 = 1 << 2;
    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the additional content is present or not.
     */
    static final int CONTENT_PRESENT = 1 << 3;

    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the edge content is added before the additional content (0) or after it (1).
     */
    static final int EDGE_CONTENT_POSITION = 1 << 4;

    /**
     * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
     * the responsive content inset is used or not.
     */
    static final int CONTENT_INSET_USED = 1 << 5;

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

    @NonNull private final Box mImpl;

    EdgeContentLayout(@NonNull Box layoutElement) {
        this.mImpl = layoutElement;
    }

    /** Builder class for {@link EdgeContentLayout}. */
    public static final class Builder implements LayoutElement.Builder {
        @NonNull private final DeviceParameters mDeviceParameters;
        @Nullable private LayoutElement mEdgeContent = null;
        @Nullable private LayoutElement mPrimaryLabelText = null;
        @Nullable private LayoutElement mSecondaryLabelText = null;
        @Nullable private LayoutElement mContent = null;
        private byte mMetadataContentByte = 0;
        private boolean mIsEdgeContentBehind = false;
        private boolean mIsResponsiveInsetEnabled = false;
        @Nullable private Float mEdgeContentThickness = null;
        @NonNull
        private DpProp mVerticalSpacerHeight =
                EDGE_CONTENT_LAYOUT_CONTENT_AND_SECONDARY_LABEL_SPACING_DP;

        /**
         * Creates a builder for the {@link EdgeContentLayout}. Custom content inside of it can
         * later be set with ({@link #setContent}.
         *
         * <p>For optimal layouts across different screen sizes and better alignment with UX
         * guidelines, it is highly recommended to call {@link #setResponsiveContentInsetEnabled}.
         */
        public Builder(@NonNull DeviceParameters deviceParameters) {
            this.mDeviceParameters = deviceParameters;
        }

        /**
         * Changes this {@link EdgeContentLayout} to better follow guidelines for type of layout
         * that has content around the edge.
         *
         * <p>These updates include:
         * 1. Using responsive insets for its content primary and secondary label by adding some
         * additional space on the sides of these elements to avoid content going off the screen
         * edge.
         * 2. Changing layout padding to responsive to better follow different screen sizes.
         * 3. Positioning primary label at a fixed place on top of the screen rather than
         * following additional content.
         *
         * <p>It is highly recommended to call this method with {@code true} when using this layout
         * to optimize it for different screen sizes.
         *
         * @throws IllegalStateException if this and
         * {@link #setEdgeContentBehindAllOtherContent(boolean)} are used together.
         */
        @NonNull
        public Builder setResponsiveContentInsetEnabled(boolean enabled) {
            if (mIsEdgeContentBehind) {
                // We don't allow mixing this method with responsiveness.
                throw new IllegalStateException(
                        "Setters setResponsiveContentInsetEnabled and "
                                + "setEdgeContentBehindAllOtherContent can't be used together. "
                                + "Please use only setResponsiveContentInsetEnabled, which will "
                                + "always place the edge content behind other content.");
            }

            this.mIsResponsiveInsetEnabled = enabled;
            if (enabled) {
                mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_INSET_USED);
            } else {
                mMetadataContentByte = (byte) (mMetadataContentByte & ~CONTENT_INSET_USED);
            }
            return this;
        }

        /**
         * Sets the thickness of the hollow edge content so that other content is correctly placed.
         * In other words, sets the space that should be reserved exclusively for the edge
         * content and not be overdrawn by other inner content.
         *
         * <p>For example, for {@link CircularProgressIndicator} or {@link
         * androidx.wear.protolayout.LayoutElementBuilders.ArcLine} elements, this should be equal
         * to their stroke width/thickness.
         *
         * <p>Note that, calling this method when responsiveness is not set with
         * {@link #setResponsiveContentInsetEnabled}, will be ignored.
         */
        @NonNull
        public Builder setEdgeContentThickness(@Dimension(unit = DP) float thickness) {
            this.mEdgeContentThickness = thickness;
            return this;
        }

        /**
         * Sets the content to be around the edges. This can be {@link CircularProgressIndicator}.
         *
         * <p>If this content is something other that {@link CircularProgressIndicator}, please add
         * its thickness with {@link #setEdgeContentThickness} for best results.
         */
        @NonNull
        public Builder setEdgeContent(@NonNull LayoutElement edgeContent) {
            this.mEdgeContent = edgeContent;
            mMetadataContentByte = (byte) (mMetadataContentByte | EDGE_CONTENT_PRESENT);
            return this;
        }

        /**
         * Sets the content in the primary label slot.
         *
         * <p>Depending on whether {@link #setResponsiveContentInsetEnabled} is set to true or
         * not, this label will be placed as following:
         * - If responsive behaviour is set, label will be above the additional content, on a fixed
         * place to ensure Tiles consistency with other layouts. Additionally, the label will
         * also have an inset to prevent it from going off the screen.
         * - If responsive behaviour is not set or called, label will be above the additional
         * content, centered in the remaining space.
         */
        @NonNull
        public Builder setPrimaryLabelTextContent(@NonNull LayoutElement primaryLabelText) {
            this.mPrimaryLabelText = primaryLabelText;
            mMetadataContentByte = (byte) (mMetadataContentByte | PRIMARY_LABEL_PRESENT);
            return this;
        }

        /**
         * Sets the content in the secondary label slot which will be below the additional content.
         * It is highly recommended to have primary label set when having secondary label.
         *
         * <p>Note that when {@link #setResponsiveContentInsetEnabled} is set to {@code true}, the
         * label will also have an inset to prevent it from going off the screen.
         */
        @NonNull
        public Builder setSecondaryLabelTextContent(@NonNull LayoutElement secondaryLabelText) {
            this.mSecondaryLabelText = secondaryLabelText;
            mMetadataContentByte = (byte) (mMetadataContentByte | SECONDARY_LABEL_PRESENT);
            return this;
        }

        /** Sets the additional content to this layout, inside of the screen. */
        @NonNull
        public Builder setContent(@NonNull LayoutElement content) {
            this.mContent = content;
            mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_PRESENT);
            return this;
        }

        /**
         * Sets the space size between the additional content and secondary label if there is any.
         * If one of those is not present, spacer is not used. If not set,
         * {@link LayoutDefaults#EDGE_CONTENT_LAYOUT_CONTENT_AND_SECONDARY_LABEL_SPACING_DP} will
         * be used.
         *
         * <p>Note that, this method should be used together with
         * {@link #setResponsiveContentInsetEnabled}, otherwise it will be ignored.
         */
        @NonNull
        public Builder setContentAndSecondaryLabelSpacing(@NonNull DpProp height) {
            this.mVerticalSpacerHeight = height;
            return this;
        }

        /**
         * Sets whether the edge content passed in with {@link #setEdgeContent} should be positioned
         * behind all other content in this layout or above it. If not set, defaults to {@code
         * false}, meaning that the edge content will be placed above all other content.
         *
         * <p>Note that, if {@link #setResponsiveContentInsetEnabled} is set to {@code true}, edge
         * content will always go behind all other content and this method call will be ignored.
         *
         * @throws IllegalStateException if this and {@link #setResponsiveContentInsetEnabled}
         * are used together.
         */
        @NonNull
        public Builder setEdgeContentBehindAllOtherContent(boolean isBehind) {
            if (mIsResponsiveInsetEnabled) {
                // We don't allow mixing this method with responsiveness.
                throw new IllegalStateException(
                        "Setters setResponsiveContentInsetEnabled and "
                                + "setEdgeContentBehindAllOtherContent can't be used together. "
                                + "Please use only setResponsiveContentInsetEnabled, which will "
                                + "always place the edge content behind other content.");
            }

            this.mIsEdgeContentBehind = isBehind;
            return this;
        }

        /** Constructs and returns {@link EdgeContentLayout} with the provided content and look. */
        @NonNull
        @Override
        public EdgeContentLayout build() {
            if (mIsResponsiveInsetEnabled && mIsEdgeContentBehind) {
                // We don't allow mixing this method with responsiveness.
                throw new IllegalStateException(
                        "Setters setResponsiveContentInsetEnabled and "
                                + "setEdgeContentBehindAllOtherContent can't be used together. "
                                + "Please use only setResponsiveContentInsetEnabled, which will "
                                + "always place the edge content behind other content.");
            }

            return mIsResponsiveInsetEnabled ? responsiveLayoutBuild() : legacyLayoutBuild();
        }

        @NonNull
        private EdgeContentLayout responsiveLayoutBuild() {
            // Calculate what is the inset box max size, i.e., the size that all content can occupy
            // without the edge content.
            // Use provided thickness if set. Otherwise, see if we can get it from
            // CircularProgressIndicator.
            float edgeContentSize = getEdgeContentSize();

            DpProp contentHeight = dp(
                    mDeviceParameters.getScreenWidthDp() - edgeContentSize);
            DpProp contentWidth = dp(
                    mDeviceParameters.getScreenHeightDp() - edgeContentSize);

            // TODO(b/321681652): Confirm with the UX if we can put 6dp as outer margin so it
            //  matches CPI.
            float outerMargin =
                    mEdgeContent instanceof CircularProgressIndicator
                            && ((CircularProgressIndicator) mEdgeContent).isOuterMarginApplied()
                            ? 0 // CPI has this margin already.
                            : EDGE_CONTENT_LAYOUT_RESPONSIVE_OUTER_MARGIN_DP;

            // Horizontal and vertical padding added to the inner content.
            float horizontalPaddingDp =
                    EDGE_CONTENT_LAYOUT_RESPONSIVE_MARGIN_HORIZONTAL_PERCENT
                            * mDeviceParameters.getScreenWidthDp();
            float verticalPaddingDp =
                    EDGE_CONTENT_LAYOUT_RESPONSIVE_MARGIN_VERTICAL_PERCENT
                            * mDeviceParameters.getScreenWidthDp();

            // Padding to restrict labels from going off the screen.
            float labelHorizontalPaddingDp =
                    mDeviceParameters.getScreenWidthDp() * LAYOUTS_LABEL_PADDING_PERCENT;

            Modifiers modifiers =
                    new Modifiers.Builder()
                            .setPadding(
                                    new Padding.Builder()
                                            .setRtlAware(true)
                                            .setStart(dp(horizontalPaddingDp))
                                            .setEnd(dp(horizontalPaddingDp))
                                            .setTop(dp(verticalPaddingDp))
                                            .setBottom(dp(verticalPaddingDp))
                                            .build())
                            .build();

            // In this variant, it's always behind so resetting the flag to 0.
            mMetadataContentByte = (byte) (mMetadataContentByte & ~EDGE_CONTENT_POSITION);
            byte[] metadata = METADATA_TAG_BASE.clone();
            metadata[FLAG_INDEX] = mMetadataContentByte;

            Box.Builder layout =
                    new Box.Builder()
                            .setWidth(dp(mDeviceParameters.getScreenWidthDp()))
                            .setHeight(dp(mDeviceParameters.getScreenHeightDp()))
                            .setModifiers(
                                    new Modifiers.Builder()
                                            .setMetadata(
                                                    new ElementMetadata.Builder()
                                                            .setTagData(metadata).build())
                                            .setPadding(
                                                    new Padding.Builder()
                                                            .setAll(dp(outerMargin))
                                                            .setRtlAware(true).build())
                                            .build());

            if (mEdgeContent != null) {
                layout.addContent(mEdgeContent);
            }

            // Contains primary label, additional content and secondary label.
            Column.Builder allInnerContent =
                    new Column.Builder()
                            .setWidth(contentWidth)
                            .setHeight(contentHeight)
                            .setModifiers(modifiers);

            if (mPrimaryLabelText != null) {
                allInnerContent.addContent(
                        insetElementWithPadding(mPrimaryLabelText, labelHorizontalPaddingDp));
                allInnerContent.addContent(
                        new Spacer.Builder()
                                .setHeight(EDGE_CONTENT_LAYOUT_RESPONSIVE_PRIMARY_LABEL_SPACING_DP)
                                .build());
            }

            // Contains additional content and secondary label with wrapped height so it can be put
            // inside of the Box to be centered. This is because primary label stays on top at
            // the fixed place, while this content should be centered in the remaining space.
            Column.Builder contentSecondaryLabel =
                    new Column.Builder().setWidth(expand()).setHeight(wrap());

            if (mContent != null) {
                contentSecondaryLabel.addContent(mContent);
            }

            if (mSecondaryLabelText != null) {
                if (mContent != null) {
                    contentSecondaryLabel.addContent(
                            new Spacer.Builder().setHeight(mVerticalSpacerHeight).build());
                }
                contentSecondaryLabel.addContent(
                        insetElementWithPadding(mSecondaryLabelText, labelHorizontalPaddingDp));
            }

            allInnerContent.addContent(
                    new Box.Builder()
                            .setWidth(expand())
                            .setHeight(expand())
                            .addContent(contentSecondaryLabel.build())
                            .build());

            layout.addContent(allInnerContent.build());

            return new EdgeContentLayout(layout.build());
        }

        private float getEdgeContentSize() {
            float edgeContentThickness =
                    mEdgeContentThickness == null
                            ?
                            // When not set, we try to get the thickness from CPI, otherwise we can
                            // only use 0.
                            (mEdgeContent instanceof CircularProgressIndicator
                                    ? ((CircularProgressIndicator) mEdgeContent)
                                    .getStrokeWidth().getValue()
                                    : 0)
                            : mEdgeContentThickness;
            return 2 * (EDGE_CONTENT_LAYOUT_RESPONSIVE_OUTER_MARGIN_DP + edgeContentThickness);
        }

        @NonNull
        private EdgeContentLayout legacyLayoutBuild() {
            float thicknessDp =
                    mEdgeContent instanceof CircularProgressIndicator
                            ? ((CircularProgressIndicator) mEdgeContent).getStrokeWidth().getValue()
                            : 0;
            float horizontalPaddingDp =
                    isRoundDevice(mDeviceParameters)
                            ? EDGE_CONTENT_LAYOUT_MARGIN_HORIZONTAL_ROUND_DP
                            : EDGE_CONTENT_LAYOUT_MARGIN_HORIZONTAL_SQUARE_DP;
            float indicatorWidth = 2 * (thicknessDp + DEFAULT_PADDING.getValue());
            float contentHeightDp = mDeviceParameters.getScreenHeightDp() - indicatorWidth;
            float contentWidthDp = mDeviceParameters.getScreenWidthDp() - indicatorWidth;

            DpProp contentHeight = dp(Math.min(contentHeightDp, contentWidthDp));
            DpProp contentWidth = dp(Math.min(contentHeightDp, contentWidthDp));

            Modifiers modifiers =
                    new Modifiers.Builder()
                            .setPadding(
                                    new Padding.Builder()
                                            .setRtlAware(true)
                                            .setStart(dp(horizontalPaddingDp))
                                            .setEnd(dp(horizontalPaddingDp))
                                            .build())
                            .build();

            if (!mIsEdgeContentBehind) {
                // If the edge content is above the additional one, then its index should be 1.
                // Otherwise it's 0.
                mMetadataContentByte = (byte) (mMetadataContentByte | EDGE_CONTENT_POSITION);
            }

            byte[] metadata = METADATA_TAG_BASE.clone();
            metadata[FLAG_INDEX] = mMetadataContentByte;
            Box.Builder mainBoxBuilder =
                    new Box.Builder()
                            .setWidth(expand())
                            .setHeight(expand())
                            .setModifiers(
                                    new Modifiers.Builder()
                                            .setMetadata(
                                                    new ElementMetadata.Builder()
                                                            .setTagData(metadata)
                                                            .build())
                                            .build())
                            .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
                            .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER);

            Column.Builder innerContentBuilder =
                    new Column.Builder()
                            .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER);

            if (mPrimaryLabelText != null) {
                innerContentBuilder.addContent(mPrimaryLabelText);
                innerContentBuilder.addContent(
                        new Spacer.Builder()
                                .setHeight(dp(EDGE_CONTENT_LAYOUT_PADDING_ABOVE_MAIN_CONTENT_DP))
                                .build());
            }

            if (mContent != null) {
                innerContentBuilder.addContent(
                        new Box.Builder()
                                .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
                                .addContent(mContent)
                                .build());
            }

            if (mSecondaryLabelText != null) {
                innerContentBuilder.addContent(
                        new Spacer.Builder()
                                .setHeight(dp(EDGE_CONTENT_LAYOUT_PADDING_BELOW_MAIN_CONTENT_DP))
                                .build());
                innerContentBuilder.addContent(mSecondaryLabelText);
            }

            Box innerContentBox =
                    new Box.Builder()
                            .setModifiers(modifiers)
                            .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
                            .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
                            .setHeight(contentHeight)
                            .setWidth(contentWidth)
                            .addContent(innerContentBuilder.build())
                            .build();

            if (mIsEdgeContentBehind) {
                if (mEdgeContent != null) {
                    mainBoxBuilder.addContent(mEdgeContent);
                }
                mainBoxBuilder.addContent(innerContentBox);
            } else {
                mainBoxBuilder.addContent(innerContentBox);
                if (mEdgeContent != null) {
                    mainBoxBuilder.addContent(mEdgeContent);
                }
            }

            return new EdgeContentLayout(mainBoxBuilder.build());
        }
    }

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

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

    /** Returns the inner content from this layout. */
    @Nullable
    public LayoutElement getContent() {
        if (!areElementsPresent(CONTENT_PRESENT)) {
            return null;
        }
        if (isResponsiveContentInsetEnabled()) {
            return getInnerColumnContentsForResponsive().get(0);
        } else {
            // By tag we know that content exists. It will be at position 0 if there is no primary
            // label, or at position 2 (primary label, spacer - content) otherwise.
            int contentPosition = areElementsPresent(PRIMARY_LABEL_PRESENT) ? 2 : 0;
            return ((Box) getInnerContent(contentPosition)).getContents().get(0);
        }
    }

    /**
     * Returns element from the inner content that is on the given index. It is a callers
     * responsibility to pass in the correct index.
     */
    private LayoutElement getInnerContent(int contentPosition) {
        return getAllContent().getContents().get(contentPosition);
    }

    /** Get the primary label content from this layout. */
    @Nullable
    public LayoutElement getPrimaryLabelTextContent() {
        if (!areElementsPresent(PRIMARY_LABEL_PRESENT)) {
            return null;
        }
        // By tag we know that primary label exists. It will always be at position 0 in the inner
        // content area.
        return isResponsiveContentInsetEnabled()
                ? ((Box) getInnerContent(0)).getContents().get(0)
                : getInnerContent(0);
    }

    /** Get the secondary label content from this layout. */
    @Nullable
    public LayoutElement getSecondaryLabelTextContent() {
        if (!areElementsPresent(SECONDARY_LABEL_PRESENT)) {
            return null;
        }
        if (isResponsiveContentInsetEnabled()) {
            List<LayoutElement> innerColumnContents = getInnerColumnContentsForResponsive();
            return ((Box) innerColumnContents.get(innerColumnContents.size() - 1))
                    .getContents().get(0);
        } else {
            // By tag we know that secondary label exists. It will always be at last position.
            List<LayoutElement> mInnerColumn = getAllContent().getContents();
            return mInnerColumn.get(mInnerColumn.size() - 1);
        }
    }

    /** Get the size of spacing between content and secondary from this layout. */
    @Dimension(unit = Dimension.DP)
    public float getContentAndSecondaryLabelSpacing() {
        if (!isResponsiveContentInsetEnabled()) {
            return EDGE_CONTENT_LAYOUT_CONTENT_AND_SECONDARY_LABEL_SPACING_DP.getValue();
        }

        List<LayoutElement> innerColumnContents = getInnerColumnContentsForResponsive();
        if (areElementsPresent(CONTENT_PRESENT) && areElementsPresent(SECONDARY_LABEL_PRESENT)) {
            LayoutElement element =
                    ((Box) innerColumnContents.get(innerColumnContents.size() - 2))
                            .getContents().get(0);
            if (element instanceof Spacer) {
                SpacerDimension height = ((Spacer) element).getHeight();
                if (height instanceof DpProp) {
                    return ((DpProp) height).getValue();
                }
            }
        }
        return EDGE_CONTENT_LAYOUT_CONTENT_AND_SECONDARY_LABEL_SPACING_DP.getValue();
    }

    /** Returns the edge content from this layout. */
    @Nullable
    public LayoutElement getEdgeContent() {
        return areElementsPresent(EDGE_CONTENT_PRESENT)
                ? mImpl.getContents().get(getEdgeContentPosition()) : null;
    }

    private int getEdgeContentPosition() {
        return isEdgeContentBehindAllOtherContent() ? 0 : 1;
    }

    /** Returns if the edge content has been placed behind the other contents. */
    public boolean isEdgeContentBehindAllOtherContent() {
        return (getMetadataTag()[FLAG_INDEX] & EDGE_CONTENT_POSITION) == 0;
    }

    /** Returns whether the contents from this layout are using responsive inset. */
    public boolean isResponsiveContentInsetEnabled() {
        return areElementsPresent(CONTENT_INSET_USED);
    }

    /** Returns the total size of the edge content including margins. */
    public float getEdgeContentThickness() {
        Column allContent = getAllContent();
        if (mImpl.getWidth() instanceof DpProp && allContent.getWidth() instanceof DpProp) {
            float edgeContentTotalThickness =
                    ((DpProp) mImpl.getWidth()).getValue()
                            - ((DpProp) allContent.getWidth()).getValue();
            return edgeContentTotalThickness / 2 - EDGE_CONTENT_LAYOUT_RESPONSIVE_OUTER_MARGIN_DP;
        }
        return 0;
    }

    /** Returns Column that may contain primary label, additional content and secondary label. */
    private Column getAllContent() {
        int contentIndex = 1 - getEdgeContentPosition();
        return (Column) (isResponsiveContentInsetEnabled()
                ? mImpl.getContents().get(areElementsPresent(EDGE_CONTENT_PRESENT)
                    ? 1 : 0)
                : ((Box) mImpl.getContents().get(contentIndex)).getContents().get(0));
    }

    /**
     * Returns all content inside of the inner Column that may contain additional content, spacer
     * and secondary label.
     */
    private List<LayoutElement> getInnerColumnContentsForResponsive() {
        return ((Column)
                ((Box)
                        getAllContent()
                                .getContents()
                                .get(areElementsPresent(PRIMARY_LABEL_PRESENT)
                                        // There's a primary label and then spacer after it
                                        // before other content in this Column.
                                        ? 2
                                        : 0))
                        .getContents()
                        .get(0))
                .getContents();
    }

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

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

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