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

// Copyright 2013 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.graphics.Bitmap;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.chrome.browser.infobar.ActionType;
import org.chromium.chrome.browser.infobar.InfoBarIdentifier;
import org.chromium.ui.modelutil.PropertyModel;

/**
 * The base class for all InfoBar classes.
 * Note that infobars expire by default when a new navigation occurs.
 * Make sure to use setExpireOnNavigation(false) if you want an infobar to be sticky.
 */
@JNINamespace("infobars")
public abstract class InfoBar implements InfoBarInteractionHandler, InfoBarUiItem {
    /** Interface for InfoBar to interact with its container. */
    public interface Container {
        /** @return True if the infobar is in front. */
        boolean isFrontInfoBar(InfoBar infoBar);

        /**
         * Remove the infobar from its container.
         * @param infoBar InfoBar to remove from the View hierarchy.
         */
        void removeInfoBar(InfoBar infoBar);

        /** Notifies that an infobar's View ({@link InfoBar#getView}) has changed. */
        void notifyInfoBarViewChanged();

        /** @return True if the container's destroy() method has been called. */
        boolean isDestroyed();
    }

    private final int mIconDrawableId;
    private final Bitmap mIconBitmap;
    private final @ColorRes int mIconTintId;
    private final CharSequence mMessage;

    private @Nullable Container mContainer;
    private @Nullable View mView;
    private @Nullable Context mContext;

    private boolean mIsDismissed;
    private boolean mControlsEnabled = true;

    private @Nullable PropertyModel mModel;

    // This points to the InfoBarAndroid class not any of its subclasses.
    private long mNativeInfoBarPtr;

    /**
     * Constructor for regular infobars.
     * @param iconDrawableId ID of the resource to use for the Icon.  If 0, no icon will be shown.
     * @param iconTintId The {@link ColorRes} used as tint for the {@code iconDrawableId}.
     * @param message The message to show in the infobar.
     * @param iconBitmap Icon to draw, in bitmap form.  Used mainly for generated icons.
     */
    public InfoBar(
            int iconDrawableId, @ColorRes int iconTintId, CharSequence message, Bitmap iconBitmap) {
        mIconDrawableId = iconDrawableId;
        mIconBitmap = iconBitmap;
        mIconTintId = iconTintId;
        mMessage = message;
    }

    /**
     * Stores a pointer to the native-side counterpart of this InfoBar.
     * @param nativeInfoBarPtr Pointer to the native InfoBarAndroid, not to its subclass.
     */
    @CalledByNative
    private final void setNativeInfoBar(long nativeInfoBarPtr) {
        assert nativeInfoBarPtr != 0;
        mNativeInfoBarPtr = nativeInfoBarPtr;
    }

    @CalledByNative
    protected void resetNativeInfoBar() {
        mNativeInfoBarPtr = 0;
    }

    /** Sets the Context used when creating the InfoBar. */
    public void setContext(Context context) {
        mContext = context;
    }

    /**
     * @return The {@link Context} used to create the InfoBar. This will be null before the InfoBar
     *         is added to an {@link InfoBarContainer}, or after the InfoBar is closed.
     */
    @Nullable
    protected Context getContext() {
        return mContext;
    }

    /**
     * Creates the View that represents the InfoBar.
     * @return The View representing the InfoBar.
     */
    public final View createView() {
        assert mContext != null;

        if (usesCompactLayout()) {
            InfoBarCompactLayout layout =
                    new InfoBarCompactLayout(
                            mContext, this, mIconDrawableId, mIconTintId, mIconBitmap);
            createCompactLayoutContent(layout);
            mView = layout;
        } else {
            InfoBarLayout layout =
                    new InfoBarLayout(
                            mContext, this, mIconDrawableId, mIconTintId, mIconBitmap, mMessage);
            createContent(layout);
            layout.onContentCreated();
            mView = layout;
        }

        return mView;
    }

    /** @return The model for this infobar if one was created. */
    @Nullable
    PropertyModel getModel() {
        return mModel;
    }

    /**
     * If this returns true, the infobar contents will be replaced with a one-line layout.
     * When overriding this, also override {@link #getAccessibilityMessage}.
     */
    protected boolean usesCompactLayout() {
        return false;
    }

    /**
     * Prepares the InfoBar for display and adds InfoBar-specific controls to the layout.
     * @param layout Layout containing all of the controls.
     */
    protected void createContent(InfoBarLayout layout) {}

    /**
     * Prepares and inserts views into an {@link InfoBarCompactLayout}.
     * {@link #usesCompactLayout} must return 'true' for this function to be called.
     * @param layout Layout to plug views into.
     */
    protected void createCompactLayoutContent(InfoBarCompactLayout layout) {}

    /**
     * Replaces the View currently shown in the infobar with the given View. Triggers the swap
     * animation via the InfoBarContainer.
     */
    protected void replaceView(View newView) {
        mView = newView;
        mContainer.notifyInfoBarViewChanged();
    }

    /** Returns the View shown in this infobar. Only valid after createView() has been called. */
    @Override
    public View getView() {
        return mView;
    }

    /**
     * Returns the accessibility message to announce when this infobar is first shown.
     * Override this if the InfoBar doesn't have {@link R.id.infobar_message}. It is usually the
     * case when it is in CompactLayout.
     */
    protected CharSequence getAccessibilityMessage(CharSequence defaultTitle) {
        return defaultTitle == null ? "" : defaultTitle;
    }

    @Override
    public CharSequence getAccessibilityText() {
        if (mView == null) return "";

        CharSequence title = null;
        TextView messageView = (TextView) mView.findViewById(R.id.infobar_message);
        if (messageView != null) {
            title = messageView.getText();
        }
        title = getAccessibilityMessage(title);
        if (title.length() > 0) {
            title = title + " ";
        }
        // TODO(crbug.com/41349249): Avoid string concatenation due to i18n.
        return title + mContext.getString(R.string.bottom_bar_screen_position);
    }

    @Override
    public int getPriority() {
        return InfoBarPriority.PAGE_TRIGGERED;
    }

    @Override
    @InfoBarIdentifier
    public int getInfoBarIdentifier() {
        if (mNativeInfoBarPtr == 0) return InfoBarIdentifier.INVALID;
        return InfoBarJni.get().getInfoBarIdentifier(mNativeInfoBarPtr, InfoBar.this);
    }

    /** @return whether the infobar actually needed closing. */
    @CalledByNative
    private boolean closeInfoBar() {
        if (!mIsDismissed) {
            mIsDismissed = true;
            if (!mContainer.isDestroyed()) {
                // If the container was destroyed, it's already been emptied of all its infobars.
                onStartedHiding();
                mContainer.removeInfoBar(this);
            }
            mContainer = null;
            mView = null;
            mContext = null;
            return true;
        }
        return false;
    }

    /**
     * @return If the infobar is the front infobar (i.e. visible and not hidden behind other
     *         infobars).
     */
    public boolean isFrontInfoBar() {
        return mContainer.isFrontInfoBar(this);
    }

    /**
     * Called just before the Java infobar has begun hiding.  Give the chance to clean up any child
     * UI that may remain open.
     */
    protected void onStartedHiding() {}

    /**
     * Returns pointer to native InfoBarAndroid instance. TODO(crbug.com/40120294): The function is
     * used in subclasses typically to get Tab reference. When Tab is modularized, replace this
     * function with the one that returns Tab reference.
     */
    protected long getNativeInfoBarPtr() {
        return mNativeInfoBarPtr;
    }

    /** Sets the Container that displays the InfoBar. */
    public void setContainer(Container container) {
        mContainer = container;
    }

    /** @return Whether or not this InfoBar is already dismissed (i.e. closed). */
    protected boolean isDismissed() {
        return mIsDismissed;
    }

    @Override
    public boolean areControlsEnabled() {
        return mControlsEnabled;
    }

    @Override
    public void setControlsEnabled(boolean state) {
        mControlsEnabled = state;
    }

    @Override
    public void onClick() {
        setControlsEnabled(false);
    }

    @Override
    public void onButtonClicked(boolean isPrimaryButton) {}

    @Override
    public void onLinkClicked() {
        if (mNativeInfoBarPtr != 0) InfoBarJni.get().onLinkClicked(mNativeInfoBarPtr, InfoBar.this);
    }

    /**
     * Performs some action related to the button being clicked.
     * @param action The type of action defined in {@link ActionType} in this class.
     */
    protected void onButtonClicked(@ActionType int action) {
        if (mNativeInfoBarPtr != 0) {
            InfoBarJni.get().onButtonClicked(mNativeInfoBarPtr, InfoBar.this, action);
        }
    }

    @Override
    public void onCloseButtonClicked() {
        if (mNativeInfoBarPtr != 0 && !mIsDismissed) {
            InfoBarJni.get().onCloseButtonClicked(mNativeInfoBarPtr, InfoBar.this);
        }
    }

    @InfoBarIdentifier
    @NativeMethods
    interface Natives {
        int getInfoBarIdentifier(long nativeInfoBarAndroid, InfoBar caller);

        void onLinkClicked(long nativeInfoBarAndroid, InfoBar caller);

        void onButtonClicked(long nativeInfoBarAndroid, InfoBar caller, int action);

        void onCloseButtonClicked(long nativeInfoBarAndroid, InfoBar caller);
    }
}