1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.infobar;
6 
7 import android.view.View;
8 import android.widget.PopupWindow.OnDismissListener;
9 
10 import androidx.annotation.StringRes;
11 import androidx.core.view.ViewCompat;
12 
13 import org.chromium.chrome.browser.infobar.InfoBarContainer.InfoBarContainerObserver;
14 import org.chromium.components.browser_ui.widget.textbubble.TextBubble;
15 import org.chromium.components.feature_engagement.FeatureConstants;
16 import org.chromium.components.infobars.InfoBar;
17 import org.chromium.components.infobars.InfoBarAnimationListener;
18 import org.chromium.components.infobars.InfoBarUiItem;
19 
20 /**
21  * A helper class to managing showing and dismissing in-product help dialogs based on which infobar
22  * is frontmost and showing.  This will show an in-product help window when a new relevant infobar
23  * becomes front-most.  If that infobar is closed or another infobar comes to the front the window
24  * will be dismissed.
25  */
26 public class IPHInfoBarSupport
27         implements OnDismissListener, InfoBarAnimationListener, InfoBarContainerObserver {
28     /** Helper class to hold all relevant display parameters for an in-product help window. */
29     public static class TrackerParameters {
TrackerParameters( String feature, @StringRes int textId, @StringRes int accessibilityTextId)30         public TrackerParameters(
31                 String feature, @StringRes int textId, @StringRes int accessibilityTextId) {
32             this.feature = feature;
33             this.textId = textId;
34             this.accessibilityTextId = accessibilityTextId;
35         }
36 
37         /** @see FeatureConstants */
38         public String feature;
39 
40         @StringRes
41         public int textId;
42 
43         @StringRes
44         public int accessibilityTextId;
45     }
46 
47     /** Helper class to manage state relating to a particular instance of an in-product window. */
48     public static class PopupState {
49         /** The View that represents the infobar that the in-product window is attached to. */
50         public View view;
51 
52         /** The bubble that is currently showing the in-product help. */
53         public TextBubble bubble;
54 
55         /** The in-product help feature that the popup relates to. */
56         public String feature;
57     }
58 
59     /**
60      * Delegate responsible for interacting with the in-product help backend and creating any
61      * {@link TextBubble}s if necessary.
62      */
63     public static interface IPHBubbleDelegate {
64         /**
65          * Will be called when a valid infobar of type {@code infoBarId} is showing and is attached
66          * to the view hierarchy.
67          * @param anchorView The {@link View} the {@link TextBubble} should be attached to.
68          * @param infoBarId  The id representing the type of infobar to potentially show an
69          *                   in-product help for.
70          * @return           {@code null} if no bubble should be shown.  Otherwise a valid
71          *                   {@link PopupState} representing the current state of the shown
72          *                   {@link TextBubble}.
73          */
createStateForInfoBar(View anchorView, @InfoBarIdentifier int infoBarId)74         PopupState createStateForInfoBar(View anchorView, @InfoBarIdentifier int infoBarId);
75 
76         /**
77          * Will be called when the {@link TextBubble} related to the currently showing infobar has
78          * been dismissed.
79          * @param state The {@link PopupState} that represents the {@link TextBubble} and state
80          *              created from an earlier call to {@link #createStateForInfoBar(View, int)}.
81          */
onPopupDismissed(PopupState state)82         void onPopupDismissed(PopupState state);
83     }
84 
85     /**
86      * The delegate responsible for interacting with external components (Creating a TextBubble and
87      * interacting with the IPH backend.
88      */
89     private final IPHBubbleDelegate mDelegate;
90 
91     /** The state of the currently showing in-product window or {@code null} if none is showing. */
92     private PopupState mCurrentState;
93 
94     /** Creates a new instance of an IPHInfoBarSupport class. */
IPHInfoBarSupport(IPHBubbleDelegate delegate)95     IPHInfoBarSupport(IPHBubbleDelegate delegate) {
96         mDelegate = delegate;
97     }
98 
99     // InfoBarAnimationListener implementation.
100     @Override
notifyAnimationFinished(int animationType)101     public void notifyAnimationFinished(int animationType) {}
102 
103     // Calling {@link TextBubble#dismiss()} will invoke {@link #onDismiss} which will
104     // set the value of {@link #mCurrentState} to null, which is what the assert checks. Since this
105     // goes through the Android SDK, FindBugs does not see this as happening, so the FindBugs
106     // warning for a field guaranteed to be non-null being checked for null equality needs to be
107     // suppressed.
108     @Override
notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar)109     public void notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar) {
110         View view = frontInfoBar == null ? null : frontInfoBar.getView();
111 
112         if (mCurrentState != null) {
113             // Clean up any old infobar if necessary.
114             if (mCurrentState.view != view) {
115                 mCurrentState.bubble.dismiss();
116                 assert mCurrentState == null;
117             }
118         }
119 
120         if (frontInfoBar == null || view == null || !ViewCompat.isAttachedToWindow(view)) return;
121 
122         mCurrentState = mDelegate.createStateForInfoBar(view, frontInfoBar.getInfoBarIdentifier());
123         if (mCurrentState == null) return;
124 
125         mCurrentState.bubble.addOnDismissListener(this);
126         mCurrentState.bubble.show();
127     }
128 
129     // InfoBarContainerObserver implementation.
130     @Override
onAddInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isFirst)131     public void onAddInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isFirst) {}
132 
133     // Calling {@link TextBubble#dismiss()} will invoke {@link #onDismiss} which will
134     // set the value of {@link #mCurrentState} to null, which is what the assert checks. Since this
135     // goes through the Android SDK, FindBugs does not see this as happening, so the FindBugs
136     // warning for a field guaranteed to be non-null being checked for null equality needs to be
137     // suppressed.
138     @Override
onRemoveInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isLast)139     public void onRemoveInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isLast) {
140         if (mCurrentState != null && infoBar.getView() == mCurrentState.view) {
141             mCurrentState.bubble.dismiss();
142             assert mCurrentState == null;
143         }
144     }
145 
146     @Override
onInfoBarContainerAttachedToWindow(boolean hasInfobars)147     public void onInfoBarContainerAttachedToWindow(boolean hasInfobars) {}
148 
149     @Override
onInfoBarContainerShownRatioChanged(InfoBarContainer container, float shownRatio)150     public void onInfoBarContainerShownRatioChanged(InfoBarContainer container, float shownRatio) {}
151 
152     // PopupWindow.OnDismissListener implementation.
153     @Override
onDismiss()154     public void onDismiss() {
155         if (mCurrentState == null) return;
156         mDelegate.onPopupDismissed(mCurrentState);
157         mCurrentState = null;
158     }
159 }
160