blob: 19907bdef2ecd7d40fe5997e80ced258b172ad87 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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.contextualsearch;
import android.graphics.Point;
import android.graphics.Rect;
import android.text.TextUtils;
import android.view.View;
import android.widget.PopupWindow.OnDismissListener;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.components.browser_ui.widget.textbubble.TextBubble;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.feature_engagement.TriggerState;
import org.chromium.ui.widget.AnchoredPopupWindow;
import org.chromium.ui.widget.RectProvider;
/**
* Helper class for displaying In-Product Help UI for Contextual Search.
*/
public class ContextualSearchIPH {
private static final int FLOATING_BUBBLE_SPACING_FACTOR = 10;
private View mParentView;
private ContextualSearchPanel mSearchPanel;
private TextBubble mHelpBubble;
private RectProvider mRectProvider;
private String mFeatureName;
private boolean mIsShowing;
private boolean mIsPositionedByPanel;
private boolean mHasUserEverEngaged;
private Point mFloatingBubbleAnchorPoint;
private OnDismissListener mDismissListener;
/**
* Constructs the helper class.
*/
ContextualSearchIPH() {}
/**
* @param searchPanel The instance of {@link ContextualSearchPanel}.
*/
void setSearchPanel(ContextualSearchPanel searchPanel) {
mSearchPanel = searchPanel;
}
/**
* @param parentView The parent view that the {@link TextBubble} will be attached to.
*/
void setParentView(View parentView) {
mParentView = parentView;
}
/**
* Called after the Contextual Search panel's animation is finished.
* @param wasActivatedByTap Whether Contextual Search was activated by tapping.
* @param profile The {@link Profile} used for {@link TrackerFactory}.
*/
void onPanelFinishedShowing(boolean wasActivatedByTap, Profile profile) {
if (!wasActivatedByTap) {
maybeShow(FeatureConstants.CONTEXTUAL_SEARCH_PROMOTE_TAP_FEATURE, profile);
maybeShow(FeatureConstants.CONTEXTUAL_SEARCH_WEB_SEARCH_FEATURE, profile);
}
}
/**
* Called after entity data is received.
* @param wasActivatedByTap Whether Contextual Search was activated by tapping.
* @param profile The {@link Profile} used for {@link TrackerFactory}.
*/
void onEntityDataReceived(boolean wasActivatedByTap, Profile profile) {
if (wasActivatedByTap) {
maybeShow(FeatureConstants.CONTEXTUAL_SEARCH_PROMOTE_PANEL_OPEN_FEATURE, profile);
}
}
/**
* Should be called after the user taps but a tap will not trigger due to longpress activation.
* @param profile The active user profile.
* @param bubbleAnchorPoint The point where the bubble arrow should be positioned.
* @param hasUserEverEngaged Whether the user has ever engaged Contextual Search by opening
* the panel.
* @param dismissListener An {@link OnDismissListener} to call when the bubble is dismissed.
*/
void onNonTriggeringTap(Profile profile, Point bubbleAnchorPoint, boolean hasUserEverEngaged,
OnDismissListener dismissListener) {
mFloatingBubbleAnchorPoint = bubbleAnchorPoint;
mHasUserEverEngaged = hasUserEverEngaged;
mDismissListener = dismissListener;
maybeShow(FeatureConstants.CONTEXTUAL_SEARCH_TAPPED_BUT_SHOULD_LONGPRESS_FEATURE, profile,
false);
}
/**
* Shows the appropriate In-Product Help UI if the conditions are met.
* @param featureName Name of the feature in IPH, look at {@link FeatureConstants}.
* @param profile The {@link Profile} used for {@link TrackerFactory}.
*/
private void maybeShow(String featureName, Profile profile) {
maybeShow(featureName, profile, true);
}
/**
* Shows the appropriate In-Product Help UI if the conditions are met.
* @param featureName Name of the feature in IPH, look at {@link FeatureConstants}.
* @param profile The {@link Profile} used for {@link TrackerFactory}.
* @param isPositionedByPanel Whether the bubble positioning should be based on the
* panel position instead of floating somewhere on the base page.
*/
private void maybeShow(String featureName, Profile profile, boolean isPositionedByPanel) {
mIsPositionedByPanel = isPositionedByPanel;
if (mIsShowing || profile == null || mParentView == null
|| mIsPositionedByPanel && mSearchPanel == null) {
return;
}
mFeatureName = featureName;
maybeShowFeaturedBubble(profile);
}
/**
* Shows a help bubble if the In-Product Help conditions are met.
* Private state members are used to determine which message to show in the bubble
* and how to position it.
* @param profile The {@link Profile} used for {@link TrackerFactory}.
*/
private void maybeShowFeaturedBubble(Profile profile) {
if (mIsPositionedByPanel && !mSearchPanel.isShowing()) return;
final Tracker tracker = TrackerFactory.getTrackerForProfile(profile);
if (!tracker.shouldTriggerHelpUI(mFeatureName)) return;
int stringId = 0;
switch (mFeatureName) {
case FeatureConstants.CONTEXTUAL_SEARCH_WEB_SEARCH_FEATURE:
stringId = R.string.contextual_search_iph_search_result;
break;
case FeatureConstants.CONTEXTUAL_SEARCH_PROMOTE_PANEL_OPEN_FEATURE:
stringId = R.string.contextual_search_iph_entity;
break;
case FeatureConstants.CONTEXTUAL_SEARCH_PROMOTE_TAP_FEATURE:
stringId = R.string.contextual_search_iph_tap;
break;
case FeatureConstants.CONTEXTUAL_SEARCH_TAPPED_BUT_SHOULD_LONGPRESS_FEATURE:
// TODO(donnd): put the engaged user variant behind a separate fieldtrial parameter
// so we can control it or collapse it later.
if (mHasUserEverEngaged) {
stringId = R.string.contextual_search_iph_touch_and_hold_engaged;
} else {
stringId = R.string.contextual_search_iph_touch_and_hold;
}
break;
}
assert stringId != 0;
assert mHelpBubble == null;
mRectProvider = new RectProvider(getHelpBubbleAnchorRect());
mHelpBubble = new TextBubble(mParentView.getContext(), mParentView, stringId, stringId,
mRectProvider, ChromeAccessibilityUtil.get().isAccessibilityEnabled());
mHelpBubble.setDismissOnTouchInteraction(true);
mHelpBubble.addOnDismissListener(() -> {
tracker.dismissed(mFeatureName);
mIsShowing = false;
mHelpBubble = null;
});
if (mDismissListener != null) {
mHelpBubble.addOnDismissListener(mDismissListener);
mDismissListener = null;
}
maybeSetPreferredOrientation();
mHelpBubble.show();
mIsShowing = true;
}
/**
* Updates the position of the help bubble if it is showing.
*/
void updateBubblePosition() {
if (!mIsShowing || mHelpBubble == null || !mHelpBubble.isShowing()) return;
mRectProvider.setRect(getHelpBubbleAnchorRect());
}
/**
* @return A {@link Rect} object that represents the appropriate anchor for {@link TextBubble}.
*/
private Rect getHelpBubbleAnchorRect() {
int yInsetPx = mParentView.getResources().getDimensionPixelOffset(
R.dimen.contextual_search_bubble_y_inset);
if (!mIsPositionedByPanel) {
// Position the bubble to point to an adjusted tap location, since there's no panel,
// just a selected word. It would be better to point to the rectangle of the selected
// word, but that's not easy to get.
int adjustFactor = shouldPositionBubbleBelowArrow() ? -1 : 1;
int yAdjust = FLOATING_BUBBLE_SPACING_FACTOR * yInsetPx * adjustFactor;
return new Rect(mFloatingBubbleAnchorPoint.x, mFloatingBubbleAnchorPoint.y + yAdjust,
mFloatingBubbleAnchorPoint.x, mFloatingBubbleAnchorPoint.y + yAdjust);
}
Rect anchorRect = mSearchPanel.getPanelRect();
anchorRect.top -= yInsetPx;
return anchorRect;
}
/** Overrides the preferred orientation if the bubble is not anchored to the panel. */
private void maybeSetPreferredOrientation() {
if (mIsPositionedByPanel) return;
mHelpBubble.setPreferredVerticalOrientation(shouldPositionBubbleBelowArrow()
? AnchoredPopupWindow.VerticalOrientation.BELOW
: AnchoredPopupWindow.VerticalOrientation.ABOVE);
}
/** @return whether the bubble should be positioned below it's arrow pointer. */
private boolean shouldPositionBubbleBelowArrow() {
// The bubble looks best when above the arrow, so we use that for most of the screen,
// but needs to appear below the arrow near the top.
return mFloatingBubbleAnchorPoint.y < mParentView.getHeight() / 3;
}
/**
* Dismisses the In-Product Help UI.
*/
void dismiss() {
if (!mIsShowing || TextUtils.isEmpty(mFeatureName)) return;
mHelpBubble.dismiss();
mIsShowing = false;
}
/**
* @return whether the bubble is currently showing for the tap-where-longpress-needed promo.
*/
boolean isShowingForTappedButShouldLongpress() {
return mIsShowing
&& FeatureConstants.CONTEXTUAL_SEARCH_TAPPED_BUT_SHOULD_LONGPRESS_FEATURE.equals(
mFeatureName);
}
/**
* Notifies the Feature Engagement backend and logs UMA metrics.
* @param profile The current {@link Profile}.
* @param wasSearchContentViewSeen Whether the Contextual Search panel was opened.
* @param wasActivatedByTap Whether the Contextual Search was activating by tapping.
* @param wasContextualCardsDataShown Whether entity cards were received.
*/
public static void doSearchFinishedNotifications(Profile profile,
boolean wasSearchContentViewSeen, boolean wasActivatedByTap,
boolean wasContextualCardsDataShown) {
Tracker tracker = TrackerFactory.getTrackerForProfile(profile);
if (wasSearchContentViewSeen) {
tracker.notifyEvent(EventConstants.CONTEXTUAL_SEARCH_PANEL_OPENED);
tracker.notifyEvent(wasActivatedByTap
? EventConstants.CONTEXTUAL_SEARCH_PANEL_OPENED_AFTER_TAP
: EventConstants.CONTEXTUAL_SEARCH_PANEL_OPENED_AFTER_LONGPRESS);
// Log whether IPH for opening the panel has been shown before.
ContextualSearchUma.logPanelOpenedIPH(
tracker.getTriggerState(
FeatureConstants.CONTEXTUAL_SEARCH_PROMOTE_PANEL_OPEN_FEATURE)
== TriggerState.HAS_BEEN_DISPLAYED);
// Log whether IPH for Contextual Search web search has been shown before.
ContextualSearchUma.logContextualSearchIPH(
tracker.getTriggerState(FeatureConstants.CONTEXTUAL_SEARCH_WEB_SEARCH_FEATURE)
== TriggerState.HAS_BEEN_DISPLAYED);
}
if (wasContextualCardsDataShown) {
tracker.notifyEvent(EventConstants.CONTEXTUAL_SEARCH_PANEL_OPENED_FOR_ENTITY);
}
}
}