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