blob: fb772e10ae77cfc986bf97162f99cd552d13deaa [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.widget.bottomsheet;
import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.widget.PopupWindow.OnDismissListener;
import org.chromium.base.Callback;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerChrome;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.widget.ViewHighlighter;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet.StateChangeReason;
import org.chromium.chrome.browser.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.content.browser.BrowserStartupController;
import org.chromium.ui.widget.ViewRectProvider;
import java.util.ArrayDeque;
/** A controller used to display various in-product help bubbles related to Chrome Home. */
public class ChromeHomeIphBubbleController {
/**
* The default duration the in-product help bubble should be visible before dismissing
* automatically.
*/
private static final int HELP_BUBBLE_TIMEOUT_DURATION_MS = 10000;
/**
* The name of the fieldtrial parameter used to determine the timeout duration for the
* in-product help bubble.
*/
private static final String HELP_BUBBLE_TIMEOUT_PARAM_NAME = "x_iph-timeout-duration-ms";
/** Cache the events before native is initialized. */
private final ArrayDeque<Integer> mEvents = new ArrayDeque<>();
private TextBubble mHelpBubble;
private LayoutManagerChrome mLayoutManager;
private View mControlContainer;
private ChromeFullscreenManager mFullscreenManager;
private BottomSheet mBottomSheet;
private Context mContext;
private boolean mNativeInitialized;
/**
* Create a new ChromeHomeIphBubbleController.
* @param context The {@link Context} used to retrieve resources.
* @param controlContainer The {@link View} used to position bubbles.
* @param bottomSheet The {@link BottomSheet} for the activity.
*/
public ChromeHomeIphBubbleController(
Context context, View controlContainer, BottomSheet bottomSheet) {
mContext = context;
mControlContainer = controlContainer;
mBottomSheet = bottomSheet;
BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.addStartupCompletedObserver(new BrowserStartupController.StartupCallback() {
@Override
public void onSuccess(boolean alreadyStarted) {
mNativeInitialized = true;
while (!mEvents.isEmpty()) {
trackEvent(mEvents.poll());
}
}
@Override
public void onFailure() {}
});
mBottomSheet.addObserver(new EmptyBottomSheetObserver() {
@Override
public void onSheetOpened(@StateChangeReason int reason) {
dismissHelpBubble();
if (mNativeInitialized) {
trackEvent(reason);
} else {
mEvents.add(reason);
}
}
@Override
public void onSheetClosed(@StateChangeReason int reason) {
showColdStartHelpBubble();
}
});
}
/**
* @param layoutManager The {@link LayoutManagerChrome} used to show and hide overview mode.
*/
public void setLayoutManagerChrome(LayoutManagerChrome layoutManager) {
mLayoutManager = layoutManager;
}
/**
* @param fullscreenManager Chrome's fullscreen manager.
*/
public void setFullscreenManager(ChromeFullscreenManager fullscreenManager) {
mFullscreenManager = fullscreenManager;
}
/**
* Show the in-product help bubble for the {@link BottomSheet} if it has not already been shown.
* This method must be called after the toolbar has had at least one layout pass.
*/
public void showColdStartHelpBubble() {
if (!mNativeInitialized) return;
// If FRE is not complete, the FRE screen is likely covering ChromeTabbedActivity so the
// help bubble should not be shown.
if (!FirstRunStatus.getFirstRunFlowComplete()) return;
Tracker tracker = TrackerFactory.getTrackerForProfile(Profile.getLastUsedProfile());
tracker.addOnInitializedCallback(new Callback<Boolean>() {
@Override
public void onResult(Boolean success) {
// Skip showing if the tracker failed to initialize.
if (!success) return;
maybeShowHelpBubble(false, false);
}
});
}
/**
* Show the in-product help bubble for the {@link BottomSheet} if conditions are right. This
* method must be called after the toolbar has had at least one layout pass and
* ChromeFeatureList has been initialized.
* @param fromMenu Whether the help bubble is being displayed in response to a click on the
* IPH menu header.
* @param fromPullToRefresh Whether the help bubble is being displayed due to a pull to refresh.
*/
public void maybeShowHelpBubble(boolean fromMenu, boolean fromPullToRefresh) {
// Skip showing if the bottom sheet is already open, the UI has not been initialized
// (indicated by mLayoutManager == null), or the tab switcher is showing.
if (mBottomSheet.isSheetOpen() || mLayoutManager == null
|| mLayoutManager.overviewVisible()) {
return;
}
// Determine which IPH feature to use for triggering the help UI.
Tracker tracker = TrackerFactory.getTrackerForProfile(Profile.getLastUsedProfile());
boolean showRefreshIph = fromPullToRefresh
&& tracker.shouldTriggerHelpUI(
FeatureConstants.CHROME_HOME_PULL_TO_REFRESH_FEATURE);
boolean showColdStartIph = !fromMenu && !fromPullToRefresh
&& tracker.shouldTriggerHelpUI(FeatureConstants.CHROME_HOME_EXPAND_FEATURE);
if (!fromMenu && !showRefreshIph && !showColdStartIph) return;
// Determine which strings to use.
boolean showAtTopOfScreen = showRefreshIph
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.CHROME_HOME_PULL_TO_REFRESH_IPH_AT_TOP);
View anchorView = mControlContainer;
int stringId = 0;
if (showRefreshIph) {
stringId = showAtTopOfScreen
? R.string.bottom_sheet_pull_to_refresh_help_bubble_accessibility_message
: R.string.bottom_sheet_pull_to_refresh_help_bubble_message;
} else {
stringId = R.string.bottom_sheet_help_bubble_message;
}
int accessibilityStringId = showRefreshIph
? R.string.bottom_sheet_pull_to_refresh_help_bubble_accessibility_message
: stringId;
// Register an overview mode observer so the bubble can be dismissed if overview mode
// is shown.
EmptyOverviewModeObserver overviewModeObserver = new EmptyOverviewModeObserver() {
@Override
public void onOverviewModeStartedShowing(boolean showToolbar) {
dismissHelpBubble();
}
};
mLayoutManager.addOverviewModeObserver(overviewModeObserver);
// Force the browser controls to stay visible while the help bubble is showing.
int persistentControlsToken =
mFullscreenManager.getBrowserVisibilityDelegate().showControlsPersistent();
// Create the help bubble and setup dismissal behavior.
View topAnchorView = (View) mBottomSheet.getParent();
OnLayoutChangeListener topAnchorLayoutChangeListener = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (left != oldLeft || right != oldRight || top != oldTop || bottom != oldBottom) {
dismissHelpBubble();
}
}
};
if (showAtTopOfScreen) {
mHelpBubble = new TextBubble(mContext, topAnchorView, stringId, accessibilityStringId,
false, getTopAnchorRect(topAnchorView));
topAnchorView.addOnLayoutChangeListener(topAnchorLayoutChangeListener);
} else {
ViewRectProvider rectProvider = new ViewRectProvider(anchorView);
int inset = mContext.getResources().getDimensionPixelSize(
R.dimen.bottom_sheet_help_bubble_inset);
rectProvider.setInsetPx(0, inset, 0, inset);
mHelpBubble = new TextBubble(
mContext, anchorView, stringId, accessibilityStringId, rectProvider);
}
if (ChromeFeatureList.isEnabled(ChromeFeatureList.CHROME_HOME_PERSISTENT_IPH)) {
int dismissTimeout = ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.CHROME_HOME_PERSISTENT_IPH, HELP_BUBBLE_TIMEOUT_PARAM_NAME,
HELP_BUBBLE_TIMEOUT_DURATION_MS);
mHelpBubble.setAutoDismissTimeout(dismissTimeout);
} else {
mHelpBubble.setDismissOnTouchInteraction(true);
}
mHelpBubble.addOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss() {
mFullscreenManager.getBrowserVisibilityDelegate().hideControlsPersistent(
persistentControlsToken);
mLayoutManager.removeOverviewModeObserver(overviewModeObserver);
if (fromMenu) {
tracker.dismissed(FeatureConstants.CHROME_HOME_MENU_HEADER_FEATURE);
} else if (fromPullToRefresh) {
tracker.dismissed(FeatureConstants.CHROME_HOME_PULL_TO_REFRESH_FEATURE);
} else {
tracker.dismissed(FeatureConstants.CHROME_HOME_EXPAND_FEATURE);
}
ViewHighlighter.turnOffHighlight(anchorView);
if (showAtTopOfScreen) {
topAnchorView.removeOnLayoutChangeListener(topAnchorLayoutChangeListener);
}
mHelpBubble = null;
}
});
// Show the bubble.
mHelpBubble.show();
}
/**
* @return The bottom sheet's help bubble if it exists.
*/
@VisibleForTesting
public @Nullable TextBubble getHelpBubbleForTests() {
return mHelpBubble;
}
/** Dismiss the help bubble if it is not null. */
private void dismissHelpBubble() {
if (mHelpBubble != null) mHelpBubble.dismiss();
}
/**
* @param topAnchorView The view used display the IPH bubble when it is shown at the top of the
* screen.
* @return A {@link Rect} used to anchor the IPH bubble.
*/
private Rect getTopAnchorRect(View topAnchorView) {
int[] locationInWindow = new int[2];
topAnchorView.getLocationInWindow(locationInWindow);
int centerPoint = locationInWindow[0] + topAnchorView.getWidth() / 2;
return new Rect(centerPoint, locationInWindow[1], centerPoint, locationInWindow[1]);
}
private void trackEvent(@StateChangeReason int reason) {
Tracker tracker = TrackerFactory.getTrackerForProfile(Profile.getLastUsedProfile());
tracker.notifyEvent(EventConstants.BOTTOM_SHEET_EXPANDED);
if (reason == StateChangeReason.SWIPE) {
tracker.notifyEvent(EventConstants.BOTTOM_SHEET_EXPANDED_FROM_SWIPE);
} else if (reason == StateChangeReason.EXPAND_BUTTON) {
tracker.notifyEvent(EventConstants.BOTTOM_SHEET_EXPANDED_FROM_BUTTON);
} else if (reason == StateChangeReason.OMNIBOX_FOCUS) {
tracker.notifyEvent(EventConstants.BOTTOM_SHEET_EXPANDED_FROM_OMNIBOX_FOCUS);
}
}
}