chromium/chrome/android/java/src/org/chromium/chrome/browser/infobar/IPHInfoBarSupport.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.chrome.browser.infobar;

import android.view.View;
import android.widget.PopupWindow.OnDismissListener;

import androidx.annotation.StringRes;
import androidx.core.view.ViewCompat;

import org.chromium.chrome.browser.infobar.InfoBarContainer.InfoBarContainerObserver;
import org.chromium.components.browser_ui.widget.textbubble.TextBubble;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.infobars.InfoBar;
import org.chromium.components.infobars.InfoBarAnimationListener;
import org.chromium.components.infobars.InfoBarUiItem;

/**
 * A helper class to managing showing and dismissing in-product help dialogs based on which infobar
 * is frontmost and showing.  This will show an in-product help window when a new relevant infobar
 * becomes front-most.  If that infobar is closed or another infobar comes to the front the window
 * will be dismissed.
 */
public class IPHInfoBarSupport
        implements OnDismissListener, InfoBarAnimationListener, InfoBarContainerObserver {
    /** Helper class to hold all relevant display parameters for an in-product help window. */
    public static class TrackerParameters {
        public TrackerParameters(
                String feature, @StringRes int textId, @StringRes int accessibilityTextId) {
            this.feature = feature;
            this.textId = textId;
            this.accessibilityTextId = accessibilityTextId;
        }

        /** @see FeatureConstants */
        public String feature;

        @StringRes public int textId;

        @StringRes public int accessibilityTextId;
    }

    /** Helper class to manage state relating to a particular instance of an in-product window. */
    public static class PopupState {
        /** The View that represents the infobar that the in-product window is attached to. */
        public View view;

        /** The bubble that is currently showing the in-product help. */
        public TextBubble bubble;

        /** The in-product help feature that the popup relates to. */
        public String feature;
    }

    /**
     * Delegate responsible for interacting with the in-product help backend and creating any
     * {@link TextBubble}s if necessary.
     */
    public static interface IPHBubbleDelegate {
        /**
         * Will be called when a valid infobar of type {@code infoBarId} is showing and is attached
         * to the view hierarchy.
         * @param anchorView The {@link View} the {@link TextBubble} should be attached to.
         * @param infoBarId  The id representing the type of infobar to potentially show an
         *                   in-product help for.
         * @return           {@code null} if no bubble should be shown.  Otherwise a valid
         *                   {@link PopupState} representing the current state of the shown
         *                   {@link TextBubble}.
         */
        PopupState createStateForInfoBar(View anchorView, @InfoBarIdentifier int infoBarId);

        /**
         * Will be called when the {@link TextBubble} related to the currently showing infobar has
         * been dismissed.
         * @param state The {@link PopupState} that represents the {@link TextBubble} and state
         *              created from an earlier call to {@link #createStateForInfoBar(View, int)}.
         */
        void onPopupDismissed(PopupState state);
    }

    /**
     * The delegate responsible for interacting with external components (Creating a TextBubble and
     * interacting with the IPH backend.
     */
    private final IPHBubbleDelegate mDelegate;

    /** The state of the currently showing in-product window or {@code null} if none is showing. */
    private PopupState mCurrentState;

    /** Creates a new instance of an IPHInfoBarSupport class. */
    IPHInfoBarSupport(IPHBubbleDelegate delegate) {
        mDelegate = delegate;
    }

    // InfoBarAnimationListener implementation.
    @Override
    public void notifyAnimationFinished(int animationType) {}

    // Calling {@link TextBubble#dismiss()} will invoke {@link #onDismiss} which will
    // set the value of {@link #mCurrentState} to null, which is what the assert checks. Since this
    // goes through the Android SDK, FindBugs does not see this as happening, so the FindBugs
    // warning for a field guaranteed to be non-null being checked for null equality needs to be
    // suppressed.
    @Override
    public void notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar) {
        View view = frontInfoBar == null ? null : frontInfoBar.getView();

        if (mCurrentState != null) {
            // Clean up any old infobar if necessary.
            if (mCurrentState.view != view) {
                mCurrentState.bubble.dismiss();
                assert mCurrentState == null;
            }
        }

        if (frontInfoBar == null || view == null || !ViewCompat.isAttachedToWindow(view)) return;

        mCurrentState = mDelegate.createStateForInfoBar(view, frontInfoBar.getInfoBarIdentifier());
        if (mCurrentState == null) return;

        mCurrentState.bubble.addOnDismissListener(this);
        mCurrentState.bubble.show();
    }

    // InfoBarContainerObserver implementation.
    @Override
    public void onAddInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isFirst) {}

    // Calling {@link TextBubble#dismiss()} will invoke {@link #onDismiss} which will
    // set the value of {@link #mCurrentState} to null, which is what the assert checks. Since this
    // goes through the Android SDK, FindBugs does not see this as happening, so the FindBugs
    // warning for a field guaranteed to be non-null being checked for null equality needs to be
    // suppressed.
    @Override
    public void onRemoveInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isLast) {
        if (mCurrentState != null && infoBar.getView() == mCurrentState.view) {
            mCurrentState.bubble.dismiss();
            assert mCurrentState == null;
        }
    }

    @Override
    public void onInfoBarContainerAttachedToWindow(boolean hasInfobars) {}

    @Override
    public void onInfoBarContainerShownRatioChanged(InfoBarContainer container, float shownRatio) {}

    // PopupWindow.OnDismissListener implementation.
    @Override
    public void onDismiss() {
        if (mCurrentState == null) return;
        mDelegate.onPopupDismissed(mCurrentState);
        mCurrentState = null;
    }
}