1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.wear.protolayout.material.layouts;
18 
19 import static androidx.annotation.Dimension.DP;
20 import static androidx.wear.protolayout.DimensionBuilders.dp;
21 import static androidx.wear.protolayout.DimensionBuilders.expand;
22 import static androidx.wear.protolayout.DimensionBuilders.wrap;
23 import static androidx.wear.protolayout.material.ChipDefaults.MIN_TAPPABLE_SQUARE_LENGTH;
24 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.DEFAULT_VERTICAL_SPACER_HEIGHT;
25 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.LAYOUTS_LABEL_PADDING_PERCENT;
26 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_PERCENT;
27 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP;
28 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP;
29 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT;
30 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT;
31 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT;
32 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT;
33 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT;
34 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT;
35 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP;
36 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP;
37 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.insetElementWithPadding;
38 import static androidx.wear.protolayout.materialcore.Helper.checkNotNull;
39 import static androidx.wear.protolayout.materialcore.Helper.checkTag;
40 import static androidx.wear.protolayout.materialcore.Helper.getMetadataTagBytes;
41 import static androidx.wear.protolayout.materialcore.Helper.getTagBytes;
42 import static androidx.wear.protolayout.materialcore.Helper.isRoundDevice;
43 
44 import android.annotation.SuppressLint;
45 
46 import androidx.annotation.Dimension;
47 import androidx.annotation.IntDef;
48 import androidx.annotation.RestrictTo;
49 import androidx.annotation.RestrictTo.Scope;
50 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
51 import androidx.wear.protolayout.DimensionBuilders.DpProp;
52 import androidx.wear.protolayout.DimensionBuilders.SpacerDimension;
53 import androidx.wear.protolayout.LayoutElementBuilders;
54 import androidx.wear.protolayout.LayoutElementBuilders.Box;
55 import androidx.wear.protolayout.LayoutElementBuilders.Column;
56 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
57 import androidx.wear.protolayout.LayoutElementBuilders.Spacer;
58 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
59 import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
60 import androidx.wear.protolayout.ModifiersBuilders.Padding;
61 import androidx.wear.protolayout.expression.Fingerprint;
62 import androidx.wear.protolayout.material.CompactChip;
63 import androidx.wear.protolayout.proto.LayoutElementProto;
64 
65 import org.jspecify.annotations.NonNull;
66 import org.jspecify.annotations.Nullable;
67 
68 import java.lang.annotation.Retention;
69 import java.lang.annotation.RetentionPolicy;
70 import java.util.Arrays;
71 import java.util.List;
72 
73 /**
74  * ProtoLayout layout that represents a suggested layout style for Material ProtoLayout with the
75  * primary (compact) chip at the bottom with the given content in the center and the recommended
76  * margin and padding applied. There is a fixed slot for an optional primary label above or optional
77  * secondary label below the main content area. Visuals and design samples can be found
78  * <a href="https://developer.android.com/design/ui/wear/guides/surfaces/tiles-layouts#layout-templates">here</a>.
79  *
80  * <p>It is highly recommended that main content has max lines between 2 and 4 (dependant on labels
81  * present), i.e.: * No labels are present: content with max 4 lines, * 1 label is present: content
82  * with max 3 lines, * 2 labels are present: content with max 2 lines.
83  *
84  * <p>When accessing the contents of a container for testing, note that this element can't be simply
85  * casted back to the original type, i.e.:
86  *
87  * <pre>{@code
88  * PrimaryLayout pl = new PrimaryLayout...
89  * Box box = new Box.Builder().addContent(pl).build();
90  *
91  * PrimaryLayout myPl = (PrimaryLayout) box.getContents().get(0);
92  * }</pre>
93  *
94  * will fail.
95  *
96  * <p>To be able to get {@link PrimaryLayout} object from any layout element, {@link
97  * #fromLayoutElement} method should be used, i.e.:
98  *
99  * <pre>{@code
100  * PrimaryLayout myPl = PrimaryLayout.fromLayoutElement(box.getContents().get(0));
101  * }</pre>
102  */
103 public class PrimaryLayout implements LayoutElement {
104     /**
105      * Prefix tool tag for Metadata in Modifiers, so we know that Box is actually a PrimaryLayout.
106      */
107     static final String METADATA_TAG_PREFIX = "PL_";
108 
109     /** Index for byte array that contains bits to check whether the contents are present or not. */
110     static final int FLAG_INDEX = METADATA_TAG_PREFIX.length();
111 
112     /**
113      * Base tool tag for Metadata in Modifiers, so we know that Box is actually a PrimaryLayout and
114      * what optional content is added.
115      */
116     static final byte[] METADATA_TAG_BASE =
117             Arrays.copyOf(getTagBytes(METADATA_TAG_PREFIX), FLAG_INDEX + 1);
118 
119     /**
120      * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
121      * the primary chip is present or not.
122      */
123     static final int CHIP_PRESENT = 0x1;
124 
125     /**
126      * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
127      * the primary label is present or not.
128      */
129     static final int PRIMARY_LABEL_PRESENT = 1 << 1;
130 
131     /**
132      * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
133      * the secondary label is present or not.
134      */
135     static final int SECONDARY_LABEL_PRESENT = 1 << 2;
136 
137     /**
138      * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
139      * the content is present or not.
140      */
141     static final int CONTENT_PRESENT = 1 << 3;
142 
143     /**
144      * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether
145      * the responsive content inset is used or not.
146      */
147     static final int CONTENT_INSET_USED = 1 << 4;
148 
149     /** Position of the primary label in its own inner column if exists. */
150     static final int PRIMARY_LABEL_POSITION = 1;
151     /** Position of the content in its own inner column. */
152     static final int CONTENT_ONLY_POSITION = 0;
153     /** Position of the primary chip in main layout column. */
154     static final int PRIMARY_CHIP_POSITION = 1;
155 
156     private final boolean mIsResponsiveInsetEnabled;
157 
158     @RestrictTo(Scope.LIBRARY)
159     @Retention(RetentionPolicy.SOURCE)
160     @IntDef(
161             flag = true,
162             value = {
163                     CHIP_PRESENT,
164                     PRIMARY_LABEL_PRESENT,
165                     SECONDARY_LABEL_PRESENT,
166                     CONTENT_PRESENT,
167                     CONTENT_INSET_USED
168             })
169     @interface ContentBits {}
170 
171     private final @NonNull Box mImpl;
172 
173     // This contains inner columns and primary chip.
174     private final @NonNull List<LayoutElement> mAllContent;
175     // This contains primary label content.
176     private final @NonNull List<LayoutElement> mPrimaryLabel;
177     // This contains optional labels, spacers and main content.
178     private final @NonNull List<LayoutElement> mContentAndSecondaryLabel;
179 
PrimaryLayout(@onNull Box layoutElement)180     PrimaryLayout(@NonNull Box layoutElement) {
181         this.mImpl = layoutElement;
182         this.mAllContent = ((Column) layoutElement.getContents().get(0)).getContents();
183         List<LayoutElement> innerContent = ((Column) mAllContent.get(0)).getContents();
184         this.mPrimaryLabel = ((Column) innerContent.get(0)).getContents();
185         this.mContentAndSecondaryLabel =
186                 ((Column) ((Box) innerContent.get(1)).getContents().get(0)).getContents();
187         this.mIsResponsiveInsetEnabled = areElementsPresent(CONTENT_INSET_USED);
188     }
189 
190     /** Builder class for {@link PrimaryLayout}. */
191     public static final class Builder implements LayoutElement.Builder {
192         private final @NonNull DeviceParameters mDeviceParameters;
193         private @Nullable LayoutElement mPrimaryChip = null;
194         private boolean mIsResponsiveInsetEnabled = false;
195         private @Nullable LayoutElement mPrimaryLabelText = null;
196         private @Nullable LayoutElement mSecondaryLabelText = null;
197         private @NonNull LayoutElement mContent = new Box.Builder().build();
198         private @NonNull DpProp mVerticalSpacerHeight = DEFAULT_VERTICAL_SPACER_HEIGHT;
199         private byte mMetadataContentByte = 0;
200 
201         /**
202          * Creates a builder for the {@link PrimaryLayout} from the given content. Content inside of
203          * it can later be set with {@link #setContent}, {@link #setPrimaryChipContent}, {@link
204          * #setPrimaryLabelTextContent} and {@link #setSecondaryLabelTextContent}.
205          *
206          * <p>For optimal layouts across different screen sizes, it is highly recommended to call
207          * {@link #setResponsiveContentInsetEnabled}.
208          */
Builder(@onNull DeviceParameters deviceParameters)209         public Builder(@NonNull DeviceParameters deviceParameters) {
210             this.mDeviceParameters = deviceParameters;
211         }
212 
213         /**
214          * Changes this {@link PrimaryLayout} to use responsive insets for its content (primary
215          * label, secondary label and primary bottom chip) by adding an additional space on
216          * the side of this element to avoid that content going off the screen edge.
217          *
218          * <p>It is highly recommended to call this method with {@code true} when using this layout
219          * to optimize it for different screen sizes.
220          */
setResponsiveContentInsetEnabled(boolean enabled)221         public @NonNull Builder setResponsiveContentInsetEnabled(boolean enabled) {
222             this.mIsResponsiveInsetEnabled = enabled;
223             if (enabled) {
224                 mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_INSET_USED);
225             } else {
226                 mMetadataContentByte = (byte) (mMetadataContentByte & ~CONTENT_INSET_USED);
227             }
228             return this;
229         }
230 
231         /**
232          * Sets the element which is in the slot at the bottom of the layout. Note that it is
233          * accepted to pass in any {@link LayoutElement}, but it is strongly recommended to add a
234          * {@link CompactChip} as the layout is optimized for it.
235          */
setPrimaryChipContent(@onNull LayoutElement compactChip)236         public @NonNull Builder setPrimaryChipContent(@NonNull LayoutElement compactChip) {
237             this.mPrimaryChip = compactChip;
238             mMetadataContentByte = (byte) (mMetadataContentByte | CHIP_PRESENT);
239             return this;
240         }
241 
242         /** Sets the content in the primary label slot which will be above the main content. */
setPrimaryLabelTextContent( @onNull LayoutElement primaryLabelText)243         public @NonNull Builder setPrimaryLabelTextContent(
244                 @NonNull LayoutElement primaryLabelText) {
245             this.mPrimaryLabelText = primaryLabelText;
246             mMetadataContentByte = (byte) (mMetadataContentByte | PRIMARY_LABEL_PRESENT);
247             return this;
248         }
249 
250         /**
251          * Sets the content in the primary label slot which will be below the main content. It is
252          * highly recommended to have primary label set when having secondary label.
253          */
setSecondaryLabelTextContent( @onNull LayoutElement secondaryLabelText)254         public @NonNull Builder setSecondaryLabelTextContent(
255                 @NonNull LayoutElement secondaryLabelText) {
256             this.mSecondaryLabelText = secondaryLabelText;
257             mMetadataContentByte = (byte) (mMetadataContentByte | SECONDARY_LABEL_PRESENT);
258             return this;
259         }
260 
261         /**
262          * Sets the additional content to this layout, above the primary chip.
263          *
264          * <p>The content slot will wrap the elements' height, so the height of the given content
265          * must be fixed or set to wrap ({@code expand} can't be used).
266          *
267          * <p>This layout has built-in horizontal margins, so the given content should have width
268          * set to {@code expand} to use all the available space, rather than an explicit width which
269          * may lead to clipping.
270          */
setContent(@onNull LayoutElement content)271         public @NonNull Builder setContent(@NonNull LayoutElement content) {
272             this.mContent = content;
273             mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_PRESENT);
274             return this;
275         }
276 
277         /**
278          * Sets the vertical spacer height which is used as a space between main content and
279          * secondary label if there is any. If not set, {@link
280          * LayoutDefaults#DEFAULT_VERTICAL_SPACER_HEIGHT} will be used.
281          */
282         // The @Dimension(unit = DP) on dp() is seemingly being ignored, so lint complains that
283         // we're passing PX to something expecting DP. Just suppress the warning for now.
284         @SuppressLint("ResourceType")
setVerticalSpacerHeight(@imensionunit = DP) float height)285         public @NonNull Builder setVerticalSpacerHeight(@Dimension(unit = DP) float height) {
286             this.mVerticalSpacerHeight = dp(height);
287             return this;
288         }
289 
290         /** Constructs and returns {@link PrimaryLayout} with the provided content and look. */
291         // The @Dimension(unit = DP) on dp() is seemingly being ignored, so lint complains that
292         // we're passing DP to something expecting PX. Just suppress the warning for now.
293         @SuppressLint("ResourceType")
294         @Override
build()295         public @NonNull PrimaryLayout build() {
296             float topPadding = getTopPadding();
297             float bottomPadding = getBottomPadding();
298             float horizontalPadding = getHorizontalPadding();
299 
300             float primaryChipHeight =
301                     mPrimaryChip != null ? MIN_TAPPABLE_SQUARE_LENGTH.getValue() : 0;
302 
303             DpProp mainContentHeight =
304                     dp(
305                             mDeviceParameters.getScreenHeightDp()
306                                     - primaryChipHeight
307                                     - bottomPadding
308                                     - topPadding);
309 
310             // Layout organization: column(column(primary label + spacer + (box(column(content +
311             // secondary label))) + chip)
312 
313             // First column that has all other content and chip.
314             Column.Builder layoutBuilder = new Column.Builder();
315 
316             // Contains primary label, main content and secondary label. Primary label will be
317             // wrapped, while other content will be expanded so it can be centered in the remaining
318             // space.
319             Column.Builder contentAreaBuilder =
320                     new Column.Builder()
321                             .setWidth(expand())
322                             .setHeight(mainContentHeight)
323                             .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER);
324 
325             // Contains main content and secondary label with wrapped height so it can be put inside
326             // of the Box to be centered.
327             Column.Builder contentSecondaryLabelBuilder =
328                     new Column.Builder()
329                             .setWidth(expand())
330                             .setHeight(wrap())
331                             .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER);
332 
333             // Needs to be in column because of the spacers.
334             Column.Builder primaryLabelBuilder =
335                     new Column.Builder().setWidth(expand()).setHeight(wrap());
336 
337             if (mPrimaryLabelText != null) {
338                 primaryLabelBuilder.addContent(
339                         new Spacer.Builder().setHeight(getPrimaryLabelTopSpacerHeight()).build());
340                 primaryLabelBuilder.addContent(maybeInsetLabel(mPrimaryLabelText));
341             }
342 
343             contentAreaBuilder.addContent(primaryLabelBuilder.build());
344 
345             contentSecondaryLabelBuilder.addContent(
346                     new Box.Builder()
347                             .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
348                             .setWidth(expand())
349                             .setHeight(wrap())
350                             .addContent(mContent)
351                             .build());
352 
353             if (mSecondaryLabelText != null) {
354                 contentSecondaryLabelBuilder.addContent(
355                         new Spacer.Builder().setHeight(mVerticalSpacerHeight).build());
356                 contentSecondaryLabelBuilder.addContent(
357                         // We only need to add extra padding to the secondary label if there's no
358                         // bottom chip.
359                         mPrimaryChip == null
360                                 ? maybeInsetLabel(mSecondaryLabelText) : mSecondaryLabelText);
361             }
362 
363             contentAreaBuilder.addContent(
364                     new Box.Builder()
365                             .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
366                             .setWidth(expand())
367                             .setHeight(expand())
368                             .addContent(contentSecondaryLabelBuilder.build())
369                             .build());
370 
371             layoutBuilder
372                     .setModifiers(
373                             new Modifiers.Builder()
374                                     .setPadding(
375                                             new Padding.Builder()
376                                                     .setRtlAware(true)
377                                                     .setStart(dp(horizontalPadding))
378                                                     .setEnd(dp(horizontalPadding))
379                                                     .setTop(dp(topPadding))
380                                                     .setBottom(dp(bottomPadding))
381                                                     .build())
382                                     .build())
383                     .setWidth(expand())
384                     .setHeight(expand())
385                     .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER);
386 
387             layoutBuilder.addContent(contentAreaBuilder.build());
388 
389             if (mPrimaryChip != null) {
390                 // Depending on setter for responsive inset, this method will return either fixed or
391                 // percentage based padding.
392                 float horizontalChipPadding = getChipHorizontalPadding();
393                 layoutBuilder.addContent(
394                         new Box.Builder()
395                                 .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
396                                 .setWidth(expand())
397                                 .setHeight(wrap())
398                                 .setModifiers(
399                                         new Modifiers.Builder()
400                                                 .setPadding(
401                                                         new Padding.Builder()
402                                                                 .setRtlAware(true)
403                                                                 .setStart(dp(horizontalChipPadding))
404                                                                 .setEnd(dp(horizontalChipPadding))
405                                                                 .build())
406                                                 .build())
407                                 .addContent(mPrimaryChip)
408                                 .build());
409             }
410 
411             byte[] metadata = METADATA_TAG_BASE.clone();
412             metadata[FLAG_INDEX] = mMetadataContentByte;
413 
414             Box.Builder element =
415                     new Box.Builder()
416                             .setWidth(expand())
417                             .setHeight(expand())
418                             .setModifiers(
419                                     new Modifiers.Builder()
420                                             .setMetadata(
421                                                     new ElementMetadata.Builder()
422                                                             .setTagData(metadata)
423                                                             .build())
424                                             .build())
425                             .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
426                             .addContent(layoutBuilder.build());
427 
428             return new PrimaryLayout(element.build());
429         }
430 
431         /**
432          * Wraps the given label (for primary or secondary label content) if responsive inset is
433          * set.
434          */
maybeInsetLabel(@onNull LayoutElement label)435         private LayoutElement maybeInsetLabel(@NonNull LayoutElement label) {
436             if (!mIsResponsiveInsetEnabled) {
437                 return label;
438             }
439 
440             return insetElementWithPadding(
441                     label, mDeviceParameters.getScreenWidthDp() * LAYOUTS_LABEL_PADDING_PERCENT);
442         }
443 
444         /**
445          * Returns the recommended bottom padding, based on percentage values in {@link
446          * LayoutDefaults}.
447          */
getBottomPadding()448         private float getBottomPadding() {
449             return mPrimaryChip != null
450                     ? (mDeviceParameters.getScreenHeightDp()
451                             * (isRoundDevice(mDeviceParameters)
452                                     ? PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT
453                                     : PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT))
454                     : getTopPadding();
455         }
456 
457         /**
458          * Returns the recommended top padding, based on percentage values in {@link
459          * LayoutDefaults}.
460          */
461         @Dimension(unit = DP)
getTopPadding()462         private float getTopPadding() {
463             return mDeviceParameters.getScreenHeightDp()
464                     * (isRoundDevice(mDeviceParameters)
465                             ? PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT
466                             : PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT);
467         }
468 
469         /**
470          * Returns the recommended horizontal padding, based on percentage values in {@link
471          * LayoutDefaults}.
472          */
473         @Dimension(unit = DP)
getHorizontalPadding()474         private float getHorizontalPadding() {
475             return mDeviceParameters.getScreenWidthDp()
476                     * (isRoundDevice(mDeviceParameters)
477                             ? PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT
478                             : PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT);
479         }
480 
481         /**
482          * Returns the recommended horizontal padding for primary chip, based on percentage values
483          * and DP values in {@link LayoutDefaults}.
484          */
485         @Dimension(unit = DP)
getChipHorizontalPadding()486         private float getChipHorizontalPadding() {
487             return mIsResponsiveInsetEnabled
488                     ? PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_PERCENT
489                         * mDeviceParameters.getScreenWidthDp()
490                     : isRoundDevice(mDeviceParameters)
491                         ? PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP
492                         : PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP;
493         }
494 
495         /** Returns the spacer height to be placed above primary label to accommodate Tile icon. */
getPrimaryLabelTopSpacerHeight()496         private @NonNull DpProp getPrimaryLabelTopSpacerHeight() {
497             return isRoundDevice(mDeviceParameters)
498                     ? PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP
499                     : PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP;
500         }
501     }
502 
503     /** Get the primary label content from this layout. */
getPrimaryLabelTextContent()504     public @Nullable LayoutElement getPrimaryLabelTextContent() {
505         if (!areElementsPresent(PRIMARY_LABEL_PRESENT)) {
506             return null;
507         }
508         LayoutElement primaryLabelWrapper = mPrimaryLabel.get(PRIMARY_LABEL_POSITION);
509         // If responsive inset are used, label passed in is wrapped in a Box.
510         return mIsResponsiveInsetEnabled
511                 ? ((Box) primaryLabelWrapper).getContents().get(0)
512                 : primaryLabelWrapper;
513     }
514 
515     /** Get the secondary label content from this layout. */
getSecondaryLabelTextContent()516     public @Nullable LayoutElement getSecondaryLabelTextContent() {
517         if (!areElementsPresent(SECONDARY_LABEL_PRESENT)) {
518             return null;
519         }
520         // By tag we know that secondary label exists. It will always be at last position.
521         LayoutElement secondaryLabelWrapper =
522                 mContentAndSecondaryLabel.get(mContentAndSecondaryLabel.size() - 1);
523         return mIsResponsiveInsetEnabled && !areElementsPresent(CHIP_PRESENT)
524                 ? ((Box) secondaryLabelWrapper).getContents().get(0)
525                 : secondaryLabelWrapper;
526     }
527 
528     /** Get the inner content from this layout. */
getContent()529     public @Nullable LayoutElement getContent() {
530         if (!areElementsPresent(CONTENT_PRESENT)) {
531             return null;
532         }
533         return ((Box) mContentAndSecondaryLabel.get(CONTENT_ONLY_POSITION)).getContents().get(0);
534     }
535 
536     /** Get the primary chip content from this layout. */
getPrimaryChipContent()537     public @Nullable LayoutElement getPrimaryChipContent() {
538         if (areElementsPresent(CHIP_PRESENT)) {
539             return ((Box) mAllContent.get(PRIMARY_CHIP_POSITION)).getContents().get(0);
540         }
541         return null;
542     }
543 
544     /** Returns whether the contents from this layout are using responsive inset. */
isResponsiveContentInsetEnabled()545     public boolean isResponsiveContentInsetEnabled() {
546         return mIsResponsiveInsetEnabled;
547     }
548 
549     /** Get the vertical spacer height from this layout. */
550     // The @Dimension(unit = DP) on getValue() is seemingly being ignored, so lint complains that
551     // we're passing PX to something expecting DP. Just suppress the warning for now.
552     @SuppressLint("ResourceType")
553     @Dimension(unit = DP)
getVerticalSpacerHeight()554     public float getVerticalSpacerHeight() {
555         if (areElementsPresent(SECONDARY_LABEL_PRESENT)) {
556             LayoutElement element = mContentAndSecondaryLabel.get(CONTENT_ONLY_POSITION + 1);
557             if (element instanceof Spacer) {
558                 SpacerDimension height = ((Spacer) element).getHeight();
559                 if (height instanceof DpProp) {
560                     return ((DpProp) height).getValue();
561                 }
562             }
563         }
564         return DEFAULT_VERTICAL_SPACER_HEIGHT.getValue();
565     }
566 
areElementsPresent(@ontentBits int elementFlag)567     private boolean areElementsPresent(@ContentBits int elementFlag) {
568         return (getMetadataTag()[FLAG_INDEX] & elementFlag) == elementFlag;
569     }
570 
571     /** Returns metadata tag set to this PrimaryLayout. */
getMetadataTag()572     byte @NonNull [] getMetadataTag() {
573         return getMetadataTagBytes(checkNotNull(checkNotNull(mImpl.getModifiers()).getMetadata()));
574     }
575 
576     /**
577      * Returns PrimaryLayout object from the given LayoutElement (e.g. one retrieved from a
578      * container's content with {@code container.getContents().get(index)}) if that element can be
579      * converted to PrimaryLayout. Otherwise, it will return null.
580      */
fromLayoutElement(@onNull LayoutElement element)581     public static @Nullable PrimaryLayout fromLayoutElement(@NonNull LayoutElement element) {
582         if (element instanceof PrimaryLayout) {
583             return (PrimaryLayout) element;
584         }
585         if (!(element instanceof Box)) {
586             return null;
587         }
588         Box boxElement = (Box) element;
589         if (!checkTag(boxElement.getModifiers(), METADATA_TAG_PREFIX, METADATA_TAG_BASE)) {
590             return null;
591         }
592         // Now we are sure that this element is a PrimaryLayout.
593         return new PrimaryLayout(boxElement);
594     }
595 
596     @Override
597     @RestrictTo(Scope.LIBRARY_GROUP)
toLayoutElementProto()598     public LayoutElementProto.@NonNull LayoutElement toLayoutElementProto() {
599         return mImpl.toLayoutElementProto();
600     }
601 
602     @Override
603     @RestrictTo(Scope.LIBRARY_GROUP)
getFingerprint()604     public @Nullable Fingerprint getFingerprint() {
605         return mImpl.getFingerprint();
606     }
607 }
608