chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarCompactLayout.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.infobars;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.ColorRes;
import androidx.annotation.StringRes;

import org.chromium.base.Callback;
import org.chromium.ui.text.NoUnderlineClickableSpan;

/**
 * Lays out controls along a line, sandwiched between an (optional) icon and close button.
 * This should only be used by the {@link InfoBar} class, and is created when the InfoBar subclass
 * declares itself to be using a compact layout via {@link InfoBar#usesCompactLayout}.
 */
public class InfoBarCompactLayout extends LinearLayout implements View.OnClickListener {
    private final InfoBarInteractionHandler mInfoBar;
    private final int mCompactInfoBarSize;
    private final int mIconWidth;
    private final View mCloseButton;

    /**
     * Constructs a compat layout for the specified infobar.
     *
     * @param context The context used to render.
     * @param infoBar {@link InfoBarInteractionHandler} that listens to events.
     * @param iconResourceId Resource ID of the icon to use for the infobar.
     * @param iconTintId The {@link ColorRes} used as tint for {@code iconResourceId}.
     * @param iconBitmap Bitmap for the icon to use, if {@code iconResourceId} is not set.
     */
    // TODO(crbug.com/40120294): ctor is made public to allow access from InfoBar. Once
    // InfoBar is modularized, restore access to package private.
    public InfoBarCompactLayout(
            Context context,
            InfoBarInteractionHandler infoBar,
            int iconResourceId,
            @ColorRes int iconTintId,
            Bitmap iconBitmap) {
        super(context);
        mInfoBar = infoBar;
        mCompactInfoBarSize =
                context.getResources().getDimensionPixelOffset(R.dimen.infobar_compact_size);
        mIconWidth = context.getResources().getDimensionPixelOffset(R.dimen.infobar_big_icon_size);

        setOrientation(LinearLayout.HORIZONTAL);
        setGravity(Gravity.CENTER_VERTICAL);

        prepareIcon(iconResourceId, iconTintId, iconBitmap);
        mCloseButton = prepareCloseButton();
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.infobar_close_button) {
            mInfoBar.onCloseButtonClicked();
        } else {
            assert false;
        }
    }

    /**
     * Inserts a view before the close button.
     *
     * @param view View to insert.
     * @param weight Weight to assign to it.
     */
    // TODO(crbug.com/40120294): addContent is made public to allow access from InfoBar. Once
    // InfoBar is modularized, restore access to protected.
    public void addContent(View view, float weight) {
        LinearLayout.LayoutParams params;
        if (weight <= 0.0f) {
            params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, mCompactInfoBarSize);
        } else {
            params = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, weight);
        }
        view.setMinimumHeight(mCompactInfoBarSize);
        params.gravity = Gravity.BOTTOM;
        addView(view, indexOfChild(mCloseButton), params);
    }

    /**
     * Adds an icon to the start of the infobar, if the infobar requires one.
     * @param iconResourceId Resource ID of the icon to use.
     * @param iconTintId The {@link ColorRes} used as tint for {@code iconResourceId}.
     * @param iconBitmap Raw {@link Bitmap} to use instead of a resource.
     */
    private void prepareIcon(int iconResourceId, @ColorRes int iconTintId, Bitmap iconBitmap) {
        ImageView iconView =
                InfoBarLayout.createIconView(getContext(), iconResourceId, iconTintId, iconBitmap);
        if (iconView != null) {
            LinearLayout.LayoutParams iconParams =
                    new LinearLayout.LayoutParams(mIconWidth, mCompactInfoBarSize);
            addView(iconView, iconParams);
        }
    }

    /** Adds a close button to the end of the infobar. */
    private View prepareCloseButton() {
        ImageButton closeButton = InfoBarLayout.createCloseButton(getContext());
        closeButton.setOnClickListener(this);
        LinearLayout.LayoutParams closeParams =
                new LinearLayout.LayoutParams(mCompactInfoBarSize, mCompactInfoBarSize);
        addView(closeButton, closeParams);
        return closeButton;
    }

    /**
     * Helps building a standard message to display in a compact InfoBar. The message can feature
     * a link to perform and action from this infobar.
     */
    public static class MessageBuilder {
        private final InfoBarCompactLayout mLayout;
        private CharSequence mMessage;
        private CharSequence mLink;

        /** @param layout The layout we are building a message view for. */
        public MessageBuilder(InfoBarCompactLayout layout) {
            mLayout = layout;
        }

        public MessageBuilder withText(CharSequence message) {
            assert mMessage == null;
            mMessage = message;

            return this;
        }

        public MessageBuilder withText(@StringRes int messageResId) {
            assert mMessage == null;
            mMessage = mLayout.getResources().getString(messageResId);

            return this;
        }

        /** Appends a link after the main message, its displayed text being the specified string. */
        public MessageBuilder withLink(CharSequence label, Callback<View> onTapCallback) {
            assert mLink == null;

            final Context context = mLayout.getContext();
            SpannableString link = new SpannableString(label);
            link.setSpan(
                    new NoUnderlineClickableSpan(context, onTapCallback),
                    0,
                    label.length(),
                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            mLink = link;

            return this;
        }

        /**
         * Appends a link after the main message, its displayed text being constructed from the
         * given resource ID.
         */
        public MessageBuilder withLink(@StringRes int textResId, Callback<View> onTapCallback) {
            final Resources resources = mLayout.getResources();
            String label = resources.getString(textResId);
            return withLink(label, onTapCallback);
        }

        /** Finalizes the message view as set up in the builder and inserts it into the layout. */
        public void buildAndInsert() {
            mLayout.addContent(build(), 1f);
        }

        /**
         * Finalizes the message view as set up in the builder. The caller is responsible for adding
         * it to the parent layout.
         */
        public View build() {
            // TODO(dgn): Should be able to handle ReaderMode and Survey infobars but they have non
            // standard interaction models (no button/link, whole bar is a button) or style (large
            // rather than default text). Revisit after snowflake review.

            assert mMessage != null;

            final int messagePadding =
                    mLayout.getResources()
                            .getDimensionPixelOffset(
                                    R.dimen.infobar_compact_message_vertical_padding);

            SpannableStringBuilder builder = new SpannableStringBuilder();
            builder.append(mMessage);
            if (mLink != null) builder.append(" ").append(mLink);

            TextView prompt = new InfoBarMessageView(mLayout.getContext());
            prompt.setTextAppearance(R.style.TextAppearance_TextMedium_Primary);
            prompt.setText(builder);
            prompt.setGravity(Gravity.CENTER_VERTICAL);
            prompt.setPadding(0, messagePadding, 0, messagePadding);

            if (mLink != null) prompt.setMovementMethod(LinkMovementMethod.getInstance());

            return prompt;
        }
    }
}