InlineSuggestionUi.java

/*
 * Copyright 2020 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.autofill.inline.v1;

import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_TEXT;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;

import android.app.PendingIntent;
import android.app.slice.Slice;
import android.app.slice.SliceItem;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.autofill.R;
import androidx.autofill.inline.UiVersions;
import androidx.autofill.inline.common.BundledStyle;
import androidx.autofill.inline.common.ImageViewStyle;
import androidx.autofill.inline.common.SlicedContent;
import androidx.autofill.inline.common.TextViewStyle;
import androidx.autofill.inline.common.ViewStyle;

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

/**
 * The entry point for building the content or style for the V1 inline suggestion UI.
 *
 * <p>The V1 UI composes of four widgets, put in order in a horizontal linear layout: start icon,
 * title, subtitle, and end icon. Some of the widgets are optional, or conditionally optional
 * based on existence of other widgets. See {@link Content.Builder#build()} for the conditions.
 *
 * <p>A default theme will be applied on the UI. The client can use {@link Style} to customize
 * the style for individual widgets as well as the overall UI background.
 *
 * <p>For Autofill provider developer, to build a content {@link Slice} that can be used as input to
 * the {@link android.service.autofill.InlinePresentation}, you may use the
 * {@link InlineSuggestionUi.Content.Builder}. For example:
 *
 * <pre class="prettyprint">
 *   public Slice createSlice(
 *       InlinePresentationSpec imeSpec,
 *       CharSequence title,
 *       CharSequence subtitle,
 *       Icon startIcon,
 *       Icon endIcon,
 *       CharSequence contentDescription,
 *       PendingIntent attribution) {
 *     // Make sure that the IME spec claims support for v1 UI template.
 *     Bundle imeStyle = imeSpec.getStyle();
 *     if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
 *       return null;
 *     }
 *
 *     // Build the content for the v1 UI.
 *     Content.Builder builder =
 *         InlineSuggestionUi.newContentBuilder(attribution)
 *           .setContentDescription(contentDescription);
 *     if(!TextUtils.isEmpty(title)) {
 *       builder.setTitle(title);
 *     }
 *     if (!TextUtils.isEmpty(subtitle)) {
 *       builder.setSubtitle(subtitle);
 *     }
 *     if (startIcon != null) {
 *       startIcon.setTintBlendMode(BlendMode.DST)
 *       builder.setStartIcon(startIcon);
 *     }
 *     if (endIcon != null) {
 *       builder.setEndIcon(endIcon);
 *     }
 *     return builder.build().getSlice();
 *   }
 * </pre>
 *
 * <p>For IME developer, to build a styles {@link Bundle} that can be used as input to the
 * {@link android.widget.inline.InlinePresentationSpec}, you may use the
 * {@link UiVersions.StylesBuilder}. For example:
 *
 * <pre class="prettyprint">
 *   public Bundle createBundle(Bundle uiExtras) {
 *     // We have styles builder, because it's possible that the IME can support multiple UI
 *     // templates in the future.
 *     StylesBuilder stylesBuilder = UiVersions.newStylesBuilder();
 *
 *     // Assuming we only want to support v1 UI template. If the provided uiExtras doesn't contain
 *     // v1, then return null.
 *     if (!UiVersions.getVersions(uiExtras).contains(UiVersions.INLINE_UI_VERSION_1)) {
 *       return null;
 *     }
 *
 *     // Create the style for v1 template.
 *     Style style = InlineSuggestionUi.newStyleBuilder()
 *         .setSingleIconChipStyle(
 *             new ViewStyle.Builder()
 *                 .setBackgroundColor(Color.TRANSPARENT)
 *                 .setPadding(0, 0, 0, 0)
 *                 .setLayoutMargin(0, 0, 0, 0)
 *                 .build())
 *         .setSingleIconChipIconStyle(
 *             new ImageViewStyle.Builder()
 *                 .setMaxWidth(actionIconSize)
 *                 .setMaxHeight(actionIconSize)
 *                 .setScaleType(ScaleType.FIT_CENTER)
 *                 .setLayoutMargin(0, 0, pinnedActionMarginEnd, 0)
 *                 .setTintList(actionIconColor)
 *                 .build())
 *         .setChipStyle(
 *             new ViewStyle.Builder()
 *                 .setBackground(
 *                     Icon.createWithResource(this, R.drawable.chip_background))
 *                 .setPadding(toPixel(13), 0, toPixel(13), 0)
 *                 .build())
 *         .setStartIconStyle(
 *             new ImageViewStyle.Builder()
 *                 .setLayoutMargin(0, 0, 0, 0)
 *                 .setTintList(chipIconColor)
 *                 .build())
 *         .setTitleStyle(
 *             new TextViewStyle.Builder()
 *                 .setLayoutMargin(toPixel(4), 0, toPixel(4), 0)
 *                 .setTextColor(Color.parseColor("#FF202124"))
 *                 .setTextSize(16)
 *                 .build())
 *         .setSubtitleStyle(
 *             new TextViewStyle.Builder()
 *                 .setLayoutMargin(0, 0, toPixel(4), 0)
 *                 .setTextColor(Color.parseColor("#99202124")) // 60% opacity
 *                 .setTextSize(14)
 *                 .build())
 *         .setEndIconStyle(
 *             new ImageViewStyle.Builder()
 *                 .setLayoutMargin(0, 0, 0, 0)
 *                 .setTintList(chipIconColor)
 *                 .build())
 *         .build();
 *
 *     // Add v1 UI style to the supported styles and return.
 *     stylesBuilder.addStyle(style);
 *     Bundle stylesBundle = stylesBuilder.build();
 *     return stylesBundle;
 *   }
 * </pre>
 *
 * <p>Alternatively, if the IME wants to use the default style, then:
 *
 * <pre class="prettyprint">
 *   public Bundle createBundle(Bundle uiExtras) {
 *     if (!UiVersions.getVersions(uiExtras).contains(UiVersions.INLINE_UI_VERSION_1)) {
 *       return null;
 *     }
 *     StylesBuilder stylesBuilder = UiVersions.newStylesBuilder();
 *     stylesBuilder.addStyle(InlineSuggestionUi.newStyleBuilder().build());
 *     return stylesBuilder.build();
 *   }
 * </pre>
 */
@RequiresApi(api = Build.VERSION_CODES.R)
public final class InlineSuggestionUi {
    private static final String TAG = "InlineSuggestionUi";

    /**
     * Returns a builder to build the content for V1 inline suggestion UI.
     *
     * <p><b>Important Note:</b> The
     * {@link android.service.autofill.AutofillService AutofillService} is responsible for keeping
     * track of the {@link PendingIntent} attribution intents it has used and cleaning them up
     * properly with {@link PendingIntent#cancel()}, or reusing them for the next set of
     * suggestions. Intents are safe to cleanup on receiving a new
     * {@link android.service.autofill.AutofillService#onFillRequest} call.
     * </p>
     *
     * @param attributionIntent invoked when the UI is long-pressed.
     * @see androidx.autofill.inline.Renderer#getAttributionIntent(Slice)
     */
    @NonNull
    public static Content.Builder newContentBuilder(@NonNull PendingIntent attributionIntent) {
        return new Content.Builder(attributionIntent);
    }

    /**
     * Returns a builder to build the style for V1 inline suggestion UI.
     */
    @NonNull
    public static Style.Builder newStyleBuilder() {
        return new Style.Builder();
    }

    /**
     * @param contentSlice the content slice for V1
     * @return the V1 content created from the slice, or null if the slice is invalid
     * @hide
     */
    @RestrictTo(LIBRARY)
    @Nullable
    public static Content fromSlice(@NonNull Slice contentSlice) {
        Content content = new Content(contentSlice);
        if (!content.isValid()) {
            Log.w(TAG, "Invalid content for " + UiVersions.INLINE_UI_VERSION_1);
            return null;
        }
        return content;
    }

    /**
     * @param styleBundle the style bundle for V1
     * @return the V1 style created from the bundle, or null if the bundle is invalid
     * @hide
     */
    @RestrictTo(LIBRARY)
    @Nullable
    public static Style fromBundle(@NonNull Bundle styleBundle) {
        Style style = new Style(styleBundle);
        if (!style.isValid()) {
            Log.w(TAG, "Invalid style for " + UiVersions.INLINE_UI_VERSION_1);
            return null;
        }
        return style;
    }

    /**
     * Renders the V1 inline suggestion view with the provided content and style.
     *
     * @hide
     */
    @RestrictTo(LIBRARY)
    @NonNull
    public static View render(@NonNull Context context, @NonNull Content content,
            @NonNull Style style) {
        context = getDefaultContextThemeWrapper(context);
        final LayoutInflater inflater = LayoutInflater.from(context);
        final ViewGroup suggestionView =
                (ViewGroup) inflater.inflate(R.layout.autofill_inline_suggestion, null);

        final ImageView startIconView =
                suggestionView.findViewById(R.id.autofill_inline_suggestion_start_icon);
        final TextView titleView =
                suggestionView.findViewById(R.id.autofill_inline_suggestion_title);
        final TextView subtitleView =
                suggestionView.findViewById(R.id.autofill_inline_suggestion_subtitle);
        final ImageView endIconView =
                suggestionView.findViewById(R.id.autofill_inline_suggestion_end_icon);

        final CharSequence title = content.getTitle();
        if (title != null) {
            titleView.setText(title);
            titleView.setVisibility(View.VISIBLE);
        }
        final CharSequence subtitle = content.getSubtitle();
        if (subtitle != null) {
            subtitleView.setText(subtitle);
            subtitleView.setVisibility(View.VISIBLE);
        }
        final Icon startIcon = content.getStartIcon();
        if (startIcon != null) {
            startIconView.setImageIcon(startIcon);
            startIconView.setVisibility(View.VISIBLE);
        }
        final Icon endIcon = content.getEndIcon();
        if (endIcon != null) {
            endIconView.setImageIcon(endIcon);
            endIconView.setVisibility(View.VISIBLE);
        }
        final CharSequence contentDescription = content.getContentDescription();
        if (!TextUtils.isEmpty(contentDescription)) {
            suggestionView.setContentDescription(contentDescription);
        }

        if (style.isValid()) {
            if (content.isSingleIconOnly()) {
                style.applyStyle(suggestionView, startIconView);
            } else {
                style.applyStyle(suggestionView, startIconView, titleView,
                        subtitleView, endIconView);
            }
        }
        return suggestionView;
    }

    /**
     * @hide
     * @see androidx.autofill.inline.Renderer#getAttributionIntent(Slice)
     * @see Content#getAttributionIntent()
     */
    @RestrictTo(LIBRARY)
    @Nullable
    public static PendingIntent getAttributionIntent(@NonNull Content content) {
        return content.getAttributionIntent();
    }

    private static Context getDefaultContextThemeWrapper(@NonNull Context context) {
        Resources.Theme theme = context.getResources().newTheme();
        theme.applyStyle(R.style.Theme_AutofillInlineSuggestion, true);
        return new ContextThemeWrapper(context, theme);
    }

    private InlineSuggestionUi() {
    }

    /**
     * Style for the V1 inline suggestion UI.
     */
    public static final class Style extends BundledStyle implements UiVersions.Style {
        private static final String KEY_STYLE_V1 = "style_v1";
        private static final String KEY_CHIP_STYLE = "chip_style";
        private static final String KEY_TITLE_STYLE = "title_style";
        private static final String KEY_SUBTITLE_STYLE = "subtitle_style";
        private static final String KEY_START_ICON_STYLE = "start_icon_style";
        private static final String KEY_END_ICON_STYLE = "end_icon_style";
        private static final String KEY_SINGLE_ICON_CHIP_STYLE = "single_icon_chip_style";
        private static final String KEY_SINGLE_ICON_CHIP_ICON_STYLE = "single_icon_chip_icon_style";
        private static final String KEY_LAYOUT_DIRECTION = "layout_direction";

        /**
         * Use {@link InlineSuggestionUi#fromBundle(Bundle)} or {@link Builder#build()} to
         * instantiate the class.
         */
        Style(@NonNull Bundle bundle) {
            super(bundle);
        }

        /**
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @NonNull
        @Override
        protected String getStyleKey() {
            return KEY_STYLE_V1;
        }

        /**
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        public void applyStyle(@NonNull View singleIconChipView,
                @NonNull ImageView singleIconView) {
            if (!isValid()) {
                return;
            }

            // layout direction
            singleIconChipView.setLayoutDirection(getLayoutDirection());

            // single icon
            if (singleIconView.getVisibility() != View.GONE) {
                ImageViewStyle singleIconViewStyle = getSingleIconChipIconStyle();
                if (singleIconViewStyle == null) {
                    singleIconViewStyle = getStartIconStyle();
                }
                if (singleIconViewStyle != null) {
                    singleIconViewStyle.applyStyleOnImageViewIfValid(singleIconView);
                }
            }
            // entire chip
            ViewStyle chipViewStyle = getSingleIconChipStyle();
            if (chipViewStyle == null) {
                chipViewStyle = getChipStyle();
            }
            if (chipViewStyle != null) {
                chipViewStyle.applyStyleOnViewIfValid(singleIconChipView);
            }
        }

        /**
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        public void applyStyle(@NonNull View chipView, @NonNull ImageView startIconView,
                @NonNull TextView titleView, @NonNull TextView subtitleView,
                @NonNull ImageView endIconView) {
            if (!isValid()) {
                return;
            }

            // layout direction
            chipView.setLayoutDirection(getLayoutDirection());

            // start icon
            if (startIconView.getVisibility() != View.GONE) {
                ImageViewStyle startIconViewStyle = getStartIconStyle();
                if (startIconViewStyle != null) {
                    startIconViewStyle.applyStyleOnImageViewIfValid(startIconView);
                }
            }
            // title
            if (titleView.getVisibility() != View.GONE) {
                TextViewStyle titleStyle = getTitleStyle();
                if (titleStyle != null) {
                    titleStyle.applyStyleOnTextViewIfValid(titleView);
                }
            }
            // subtitle
            if (subtitleView.getVisibility() != View.GONE) {
                TextViewStyle subtitleStyle = getSubtitleStyle();
                if (subtitleStyle != null) {
                    subtitleStyle.applyStyleOnTextViewIfValid(subtitleView);
                }
            }
            // end icon
            if (endIconView.getVisibility() != View.GONE) {
                ImageViewStyle endIconViewStyle = getEndIconStyle();
                if (endIconViewStyle != null) {
                    endIconViewStyle.applyStyleOnImageViewIfValid(endIconView);
                }
            }
            // entire chip
            ViewStyle chipViewStyle = getChipStyle();
            if (chipViewStyle != null) {
                chipViewStyle.applyStyleOnViewIfValid(chipView);
            }
        }

        /**
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @NonNull
        @Override
        public String getVersion() {
            return UiVersions.INLINE_UI_VERSION_1;
        }

        /**
         * @see Builder#setLayoutDirection(int)
         */
        public int getLayoutDirection() {
            int layoutDirection = mBundle.getInt(KEY_LAYOUT_DIRECTION, View.LAYOUT_DIRECTION_LTR);
            if (layoutDirection != View.LAYOUT_DIRECTION_LTR
                    && layoutDirection != View.LAYOUT_DIRECTION_RTL) {
                layoutDirection = View.LAYOUT_DIRECTION_LTR;
            }
            return layoutDirection;
        }

        /**
         * @see Builder#setChipStyle(ViewStyle)
         */
        @Nullable
        public ViewStyle getChipStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_CHIP_STYLE);
            return styleBundle == null ? null : new ViewStyle(styleBundle);
        }

        /**
         * @see Builder#setTitleStyle(TextViewStyle)
         */
        @Nullable
        public TextViewStyle getTitleStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_TITLE_STYLE);
            return styleBundle == null ? null : new TextViewStyle(styleBundle);
        }

        /**
         * @see Builder#setSubtitleStyle(TextViewStyle)
         */
        @Nullable
        public TextViewStyle getSubtitleStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_SUBTITLE_STYLE);
            return styleBundle == null ? null : new TextViewStyle(styleBundle);
        }

        /**
         * @see Builder#setStartIconStyle(ImageViewStyle)
         */
        @Nullable
        public ImageViewStyle getStartIconStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_START_ICON_STYLE);
            return styleBundle == null ? null : new ImageViewStyle(styleBundle);
        }

        /**
         * @see Builder#setEndIconStyle(ImageViewStyle)
         */
        @Nullable
        public ImageViewStyle getEndIconStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_END_ICON_STYLE);
            return styleBundle == null ? null : new ImageViewStyle(styleBundle);
        }

        /**
         * @see Builder#setSingleIconChipStyle(ViewStyle)
         */
        @Nullable
        public ViewStyle getSingleIconChipStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_SINGLE_ICON_CHIP_STYLE);
            return styleBundle == null ? null : new ViewStyle(styleBundle);
        }

        /**
         * @see Builder#setSingleIconChipIconStyle(ImageViewStyle)
         */
        @Nullable
        public ImageViewStyle getSingleIconChipIconStyle() {
            Bundle styleBundle = mBundle.getBundle(KEY_SINGLE_ICON_CHIP_ICON_STYLE);
            return styleBundle == null ? null : new ImageViewStyle(styleBundle);
        }

        /**
         * Builder for the {@link Style}.
         */
        public static final class Builder extends BundledStyle.Builder<Style> {

            /**
             * Use {@link InlineSuggestionUi#newStyleBuilder()} to instantiate this class.
             */
            Builder() {
                super(KEY_STYLE_V1);
            }

            /**
             * Sets the layout direction for the UI.
             *
             * <p>Note that the process that renders the UI needs to have
             * {@code android:supportsRtl="true"} for this to take effect.
             *
             * @param layoutDirection the layout direction to set. Should be one of:
             *                        {@link View#LAYOUT_DIRECTION_LTR},
             *                        {@link View#LAYOUT_DIRECTION_RTL}.
             *
             * @see View#setLayoutDirection(int)
             */
            @NonNull
            public Builder setLayoutDirection(int layoutDirection) {
                mBundle.putInt(KEY_LAYOUT_DIRECTION, layoutDirection);
                return this;
            }

            /**
             * Sets the chip style.
             *
             * <p>See {@link #setSingleIconChipStyle(ViewStyle)} for more information about setting
             * a special chip style for the case where the entire chip is a single icon.
             */
            @NonNull
            public Builder setChipStyle(@NonNull ViewStyle chipStyle) {
                chipStyle.assertIsValid();
                mBundle.putBundle(KEY_CHIP_STYLE, chipStyle.getBundle());
                return this;
            }

            /**
             * Sets the title style.
             */
            @NonNull
            public Builder setTitleStyle(@NonNull TextViewStyle titleStyle) {
                titleStyle.assertIsValid();
                mBundle.putBundle(KEY_TITLE_STYLE, titleStyle.getBundle());
                return this;
            }

            /**
             * Sets the subtitle style.
             */
            @NonNull
            public Builder setSubtitleStyle(@NonNull TextViewStyle subtitleStyle) {
                subtitleStyle.assertIsValid();
                mBundle.putBundle(KEY_SUBTITLE_STYLE, subtitleStyle.getBundle());
                return this;
            }

            /**
             * Sets the start icon style.
             *
             * <p>See {@link #setSingleIconChipIconStyle(ImageViewStyle)} for more information
             * about setting a special icon style for the case where the entire chip is a single
             * icon.
             */
            @NonNull
            public Builder setStartIconStyle(@NonNull ImageViewStyle startIconStyle) {
                startIconStyle.assertIsValid();
                mBundle.putBundle(KEY_START_ICON_STYLE, startIconStyle.getBundle());
                return this;
            }

            /**
             * Sets the end icon style.
             */
            @NonNull
            public Builder setEndIconStyle(@NonNull ImageViewStyle endIconStyle) {
                endIconStyle.assertIsValid();
                mBundle.putBundle(KEY_END_ICON_STYLE, endIconStyle.getBundle());
                return this;
            }

            /**
             * Sets the chip style for the case where there is a single icon and no text. If not
             * provided, will fallback to use the chip style provided by {@link #setChipStyle
             * (ViewStyle)}.
             */
            @NonNull
            public Builder setSingleIconChipStyle(@NonNull ViewStyle chipStyle) {
                chipStyle.assertIsValid();
                mBundle.putBundle(KEY_SINGLE_ICON_CHIP_STYLE, chipStyle.getBundle());
                return this;
            }

            /**
             * Sets the icon style for the case where there is a single icon and no text in the
             * chip. If not provided, will fallback to use the icon style provided by
             * {@link #setStartIconStyle(ImageViewStyle)}
             */
            @NonNull
            public Builder setSingleIconChipIconStyle(@NonNull ImageViewStyle iconStyle) {
                iconStyle.assertIsValid();
                mBundle.putBundle(KEY_SINGLE_ICON_CHIP_ICON_STYLE, iconStyle.getBundle());
                return this;
            }

            @NonNull
            @Override
            public Style build() {
                return new Style(mBundle);
            }
        }
    }

    /**
     * Content for the V1 inline suggestion UI.
     */
    public static final class Content extends SlicedContent {
        static final String HINT_INLINE_TITLE = "inline_title";
        static final String HINT_INLINE_SUBTITLE = "inline_subtitle";
        static final String HINT_INLINE_START_ICON = "inline_start_icon";
        static final String HINT_INLINE_END_ICON = "inline_end_icon";
        static final String HINT_INLINE_ATTRIBUTION_INTENT = "inline_attribution";
        static final String HINT_INLINE_CONTENT_DESCRIPTION = "inline_content_description";

        @Nullable
        private Icon mStartIcon;
        @Nullable
        private Icon mEndIcon;
        @Nullable
        private CharSequence mTitle;
        @Nullable
        private CharSequence mSubtitle;
        @Nullable
        private PendingIntent mAttributionIntent;
        @Nullable
        private CharSequence mContentDescription;

        /**
         * Use {@link InlineSuggestionUi#fromSlice(Slice)} or {@link Builder#build()} to
         * instantiate this class.
         */
        Content(@NonNull Slice slice) {
            super(slice);
            for (SliceItem sliceItem : slice.getItems()) {
                final String itemType = itemType(sliceItem);
                if (itemType == null) {
                    continue;
                }
                switch (itemType) {
                    case HINT_INLINE_TITLE:
                        mTitle = sliceItem.getText().toString();
                        break;
                    case HINT_INLINE_SUBTITLE:
                        mSubtitle = sliceItem.getText().toString();
                        break;
                    case HINT_INLINE_START_ICON:
                        mStartIcon = sliceItem.getIcon();
                        break;
                    case HINT_INLINE_END_ICON:
                        mEndIcon = sliceItem.getIcon();
                        break;
                    case HINT_INLINE_ATTRIBUTION_INTENT:
                        mAttributionIntent = sliceItem.getAction();
                        break;
                    case HINT_INLINE_CONTENT_DESCRIPTION:
                        mContentDescription = sliceItem.getText();
                        break;
                    default:
                        break;
                }
            }
        }

        boolean isSingleIconOnly() {
            return mStartIcon != null && mTitle == null && mSubtitle == null && mEndIcon == null;
        }

        /**
         * @see Builder#setTitle(CharSequence)
         */
        @Nullable
        public CharSequence getTitle() {
            return mTitle;
        }

        /**
         * @see Builder#setSubtitle(CharSequence)
         */
        @Nullable
        public CharSequence getSubtitle() {
            return mSubtitle;
        }

        /**
         * @see Builder#setStartIcon(Icon)
         */
        @Nullable
        public Icon getStartIcon() {
            return mStartIcon;
        }

        /**
         * @see Builder#setEndIcon(Icon)
         */
        @Nullable
        public Icon getEndIcon() {
            return mEndIcon;
        }

        /**
         * @see Builder#setContentDescription(CharSequence)
         */
        @Nullable
        public CharSequence getContentDescription() {
            return mContentDescription;
        }

        /**
         * @see InlineSuggestionUi#newContentBuilder(PendingIntent)
         */
        @Nullable
        @Override
        public PendingIntent getAttributionIntent() {
            return mAttributionIntent;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        @Override
        public boolean isValid() {
            return UiVersions.INLINE_UI_VERSION_1.equals(SlicedContent.getVersion(mSlice));
        }

        @Nullable
        private static String itemType(SliceItem sliceItem) {
            switch (sliceItem.getFormat()) {
                case FORMAT_IMAGE:
                    if (sliceItem.getIcon() == null) {
                        return null;
                    }
                    if (sliceItem.getHints().contains(HINT_INLINE_START_ICON)) {
                        return HINT_INLINE_START_ICON;
                    } else if (sliceItem.getHints().contains(HINT_INLINE_END_ICON)) {
                        return HINT_INLINE_END_ICON;
                    }
                    break;
                case FORMAT_TEXT:
                    if (TextUtils.isEmpty(sliceItem.getText())) {
                        return null;
                    }
                    if (sliceItem.getHints().contains(HINT_INLINE_TITLE)) {
                        return HINT_INLINE_TITLE;
                    } else if (sliceItem.getHints().contains(HINT_INLINE_SUBTITLE)) {
                        return HINT_INLINE_SUBTITLE;
                    } else if (sliceItem.getHints().contains(HINT_INLINE_CONTENT_DESCRIPTION)) {
                        return HINT_INLINE_CONTENT_DESCRIPTION;
                    }
                    break;
                case FORMAT_ACTION:
                    if (sliceItem.getAction() != null && sliceItem.getHints().contains(
                            HINT_INLINE_ATTRIBUTION_INTENT)) {
                        return HINT_INLINE_ATTRIBUTION_INTENT;
                    }
                    break;
                default:
                    return null;
            }
            return null;
        }

        /**
         * Builder for the {@link Content}.
         */
        public static final class Builder extends SlicedContent.Builder<Content> {
            @NonNull
            private final PendingIntent mAttributionIntent;
            @Nullable
            private Icon mStartIcon;
            @Nullable
            private Icon mEndIcon;
            @Nullable
            private CharSequence mTitle;
            @Nullable
            private CharSequence mSubtitle;
            @Nullable
            private CharSequence mContentDescription;
            @Nullable
            private List<String> mHints;

            /**
             * Use {@link InlineSuggestionUi#newContentBuilder(PendingIntent)} to instantiate
             * this class.
             *
             * @param attributionIntent invoked when the UI is long-pressed.
             * @see androidx.autofill.inline.Renderer#getAttributionIntent(Slice)
             */
            Builder(@NonNull PendingIntent attributionIntent) {
                super(UiVersions.INLINE_UI_VERSION_1);
                mAttributionIntent = attributionIntent;
            }

            /**
             * Sets the title of the suggestion UI.
             *
             * @param title displayed as title of slice.
             */
            @NonNull
            public Builder setTitle(@NonNull CharSequence title) {
                mTitle = title;
                return this;
            }

            /**
             * Sets the subtitle of the suggestion UI.
             *
             * @param subtitle displayed as subtitle of slice.
             */
            @NonNull
            public Builder setSubtitle(@NonNull CharSequence subtitle) {
                mSubtitle = subtitle;
                return this;
            }

            /**
             * Sets the start icon of the suggestion UI.
             *
             * <p>Note that the {@link ImageViewStyle} style may specify the tint list to be
             * applied on the icon. If you don't want that, you may disable it by calling {@code
             * Icon#setTintBlendMode(BlendMode.DST)}.
             *
             * @param startIcon {@link Icon} resource displayed at start of slice.
             */
            @NonNull
            public Builder setStartIcon(@NonNull Icon startIcon) {
                mStartIcon = startIcon;
                return this;
            }

            /**
             * Sets the end icon of the suggestion UI.
             *
             * <p>Note that the {@link ImageViewStyle} style may specify the tint list to be
             * applied on the icon. If you don't want that, you may disable it by calling {@code
             * Icon#setTintBlendMode(BlendMode.DST)}.
             *
             * @param endIcon {@link Icon} resource displayed at end of slice.
             */
            @NonNull
            public Builder setEndIcon(@NonNull Icon endIcon) {
                mEndIcon = endIcon;
                return this;
            }

            /**
             * Sets the content description for the suggestion view.
             *
             * @param contentDescription the content description.
             * @see View#setContentDescription(CharSequence)
             */
            @NonNull
            public Builder setContentDescription(@NonNull CharSequence contentDescription) {
                mContentDescription = contentDescription;
                return this;
            }

            /**
             * Sets hints to indicate the kind of data in the suggestion.
             *
             * @param hints defined in {@link androidx.autofill.inline.SuggestionHintConstants}
             */
            @NonNull
            public Builder setHints(@NonNull List<String> hints) {
                mHints = hints;
                return this;
            }

            @NonNull
            @Override
            public Content build() {
                if (mTitle == null && mStartIcon == null && mEndIcon == null && mSubtitle == null) {
                    throw new IllegalStateException(
                            "Title, subtitle, start icon, end icon are all null. "
                                    + "Please set value for at least one of them");
                }
                if (mTitle == null && mSubtitle != null) {
                    throw new IllegalStateException(
                            "Cannot set the subtitle without setting the title.");
                }
                if (mAttributionIntent == null) {
                    throw new IllegalStateException("Attribution intent cannot be null.");
                }
                if (mStartIcon != null) {
                    mSliceBuilder.addIcon(mStartIcon, null,
                            Collections.singletonList(HINT_INLINE_START_ICON));
                }
                if (mTitle != null) {
                    mSliceBuilder.addText(mTitle, null,
                            Collections.singletonList(HINT_INLINE_TITLE));
                }
                if (mSubtitle != null) {
                    mSliceBuilder.addText(mSubtitle, null,
                            Collections.singletonList(HINT_INLINE_SUBTITLE));
                }
                if (mEndIcon != null) {
                    mSliceBuilder.addIcon(mEndIcon, null,
                            Collections.singletonList(HINT_INLINE_END_ICON));
                }
                if (mAttributionIntent != null) {
                    mSliceBuilder.addAction(mAttributionIntent, new Slice.Builder(
                                    mSliceBuilder).addHints(
                            Collections.singletonList(HINT_INLINE_ATTRIBUTION_INTENT)).build(),
                            null);
                }
                if (mContentDescription != null) {
                    mSliceBuilder.addText(mContentDescription, null,
                            Collections.singletonList(HINT_INLINE_CONTENT_DESCRIPTION));
                }
                if (mHints != null) {
                    mSliceBuilder.addHints(mHints);
                }
                return new Content(mSliceBuilder.build());
            }
        }
    }
}