chromium/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarButtonInProductHelpController.java

// Copyright 2018 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.toolbar;

import android.app.Activity;
import android.os.Handler;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.BuildInfo;
import org.chromium.base.TraceEvent;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.bookmarks.PowerBookmarkUtils;
import org.chromium.chrome.browser.commerce.ShoppingFeatures;
import org.chromium.chrome.browser.download.DownloadUtils;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.screenshot_monitor.ScreenshotMonitor;
import org.chromium.chrome.browser.screenshot_monitor.ScreenshotMonitorDelegate;
import org.chromium.chrome.browser.screenshot_monitor.ScreenshotMonitorImpl;
import org.chromium.chrome.browser.screenshot_monitor.ScreenshotTabObserver;
import org.chromium.chrome.browser.tab.CurrentTabObserver;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.translate.TranslateBridge;
import org.chromium.chrome.browser.translate.TranslateUtils;
import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.appmenu.AppMenuPropertiesDelegate;
import org.chromium.chrome.browser.user_education.IPHCommandBuilder;
import org.chromium.chrome.browser.user_education.UserEducationHelper;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;

/**
 * A helper class for IPH shown on the toolbar. TODO(crbug.com/40585866): Remove feature-specific
 * IPH from here.
 */
public class ToolbarButtonInProductHelpController
        implements ScreenshotMonitorDelegate, PauseResumeWithNativeObserver {
    private final CurrentTabObserver mPageLoadObserver;
    private final Activity mActivity;
    private final WindowAndroid mWindowAndroid;
    private final ActivityLifecycleDispatcher mLifecycleDispatcher;
    private final AppMenuPropertiesDelegate mAppMenuPropertiesDelegate;
    @Nullable private ScreenshotMonitor mScreenshotMonitor;
    private final View mMenuButtonAnchorView;
    private final View mSecurityIconAnchorView;
    private final AppMenuHandler mAppMenuHandler;
    private final UserEducationHelper mUserEducationHelper;
    private final Profile mProfile;
    private final Supplier<Tab> mCurrentTabSupplier;
    private final Supplier<Boolean> mIsInOverviewModeSupplier;

    /**
     * @param activity {@link Activity} on which this class runs.
     * @param windowAndroid {@link WindowAndroid} for the current Activity.
     * @param appMenuCoordinator {@link AppMenuCoordinator} whose visual state is to be updated
     *     accordingly.
     * @param lifecycleDispatcher {@link LifecycleDispatcher} that helps observe activity lifecycle.
     * @param profile The current Profile.
     * @param tabSupplier An observable supplier of the current {@link Tab}.
     * @param isInOverviewModeSupplier Supplies whether the app is in overview mode.
     * @param menuButtonAnchorView The menu button view to serve as an anchor.
     * @param securityIconAnchorView The security icon to serve as an anchor.
     */
    public ToolbarButtonInProductHelpController(
            @NonNull Activity activity,
            @NonNull WindowAndroid windowAndroid,
            @NonNull AppMenuCoordinator appMenuCoordinator,
            @NonNull ActivityLifecycleDispatcher lifecycleDispatcher,
            @NonNull Profile profile,
            @NonNull ObservableSupplier<Tab> tabSupplier,
            @NonNull Supplier<Boolean> isInOverviewModeSupplier,
            @NonNull View menuButtonAnchorView,
            @NonNull View securityIconAnchorView) {
        mActivity = activity;
        mWindowAndroid = windowAndroid;
        mAppMenuHandler = appMenuCoordinator.getAppMenuHandler();
        mAppMenuPropertiesDelegate = appMenuCoordinator.getAppMenuPropertiesDelegate();
        mMenuButtonAnchorView = menuButtonAnchorView;
        mSecurityIconAnchorView = securityIconAnchorView;
        mIsInOverviewModeSupplier = isInOverviewModeSupplier;
        mUserEducationHelper = new UserEducationHelper(mActivity, profile, new Handler());
        if (!BuildInfo.getInstance().isAutomotive) {
            mScreenshotMonitor = new ScreenshotMonitorImpl(this, mActivity);
        }
        mLifecycleDispatcher = lifecycleDispatcher;
        mLifecycleDispatcher.register(this);
        mProfile = profile;
        mCurrentTabSupplier = tabSupplier;
        mPageLoadObserver =
                new CurrentTabObserver(
                        tabSupplier,
                        new EmptyTabObserver() {
                            @Override
                            public void onPageLoadFinished(Tab tab, GURL url) {
                                // Part of scroll jank investigation http://crbug.com/1311003. Will
                                // remove TraceEvent after the investigation is complete.
                                try (TraceEvent te =
                                        TraceEvent.scoped(
                                                "ToolbarButtonInProductHelpController::onPageLoadFinished")) {
                                    if (tab.isShowingErrorPage()) {
                                        handleIPHForErrorPageShown(tab);
                                        return;
                                    }

                                    handleIPHForSuccessfulPageLoad(tab);
                                }
                            }

                            private void handleIPHForSuccessfulPageLoad(final Tab tab) {
                                showDownloadPageTextBubble(
                                        tab, FeatureConstants.DOWNLOAD_PAGE_FEATURE);
                                showTranslateMenuButtonTextBubble(tab);
                                showPriceTrackingIPH(tab);
                            }

                            private void handleIPHForErrorPageShown(Tab tab) {
                                if (DeviceFormFactor.isWindowOnTablet(mWindowAndroid)) {
                                    return;
                                }

                                OfflinePageBridge bridge =
                                        OfflinePageBridge.getForProfile(tab.getProfile());
                                if (bridge == null
                                        || !bridge.isShowingDownloadButtonInErrorPage(
                                                tab.getWebContents())) {
                                    return;
                                }

                                Tracker tracker =
                                        TrackerFactory.getTrackerForProfile(
                                                Profile.fromWebContents(tab.getWebContents()));
                                tracker.notifyEvent(EventConstants.USER_HAS_SEEN_DINO);
                            }
                        },
                        /* swapCallback= */ null);
    }

    public void destroy() {
        mPageLoadObserver.destroy();
        mLifecycleDispatcher.unregister(this);
    }

    /**
     * Attempt to show the IPH for price tracking.
     * @param tab The tab currently being displayed to the user.
     */
    private void showPriceTrackingIPH(Tab tab) {
        if (tab == null || tab.getWebContents() == null) return;

        if (!ShoppingFeatures.isShoppingListEligible(tab.getProfile())
                || !PowerBookmarkUtils.isPriceTrackingEligible(tab)) {
            return;
        }

        mUserEducationHelper.requestShowIPH(
                new IPHCommandBuilder(
                                mActivity.getResources(),
                                FeatureConstants.SHOPPING_LIST_MENU_ITEM_FEATURE,
                                R.string.iph_price_tracking_menu_item,
                                R.string.iph_price_tracking_menu_item_accessibility)
                        .setAnchorView(mMenuButtonAnchorView)
                        .setOnShowCallback(
                                () ->
                                        turnOnHighlightForMenuItem(
                                                R.id.enable_price_tracking_menu_id))
                        .setOnDismissCallback(this::turnOffHighlightForMenuItem)
                        .build());
    }

    /** Attempts to show an IPH text bubble for download continuing. */
    public void showDownloadContinuingIPH() {
        mUserEducationHelper.requestShowIPH(
                new IPHCommandBuilder(
                                mActivity.getResources(),
                                FeatureConstants.DOWNLOAD_INFOBAR_DOWNLOAD_CONTINUING_FEATURE,
                                R.string.iph_download_infobar_download_continuing_text,
                                R.string.iph_download_infobar_download_continuing_text)
                        .setAnchorView(mMenuButtonAnchorView)
                        .setOnShowCallback(() -> turnOnHighlightForMenuItem(R.id.downloads_menu_id))
                        .setOnDismissCallback(this::turnOffHighlightForMenuItem)
                        .build());
    }

    /** Attempts to show an IPH text bubble for those that trigger on a cold start. */
    public void showColdStartIPH() {
        showDownloadHomeIPH();
    }

    // Overridden public methods.
    @Override
    public void onResumeWithNative() {
        // Part of the (more runtime-related) check to determine whether to trigger help UI is
        // left until onScreenshotTaken() since it is less expensive to keep monitoring on and
        // check when the help UI is accessed than it is to start/stop monitoring per tab change
        // (e.g. tab switch or in overview mode).
        if (DeviceFormFactor.isWindowOnTablet(mWindowAndroid)) return;
        if (mScreenshotMonitor != null) mScreenshotMonitor.startMonitoring();
    }

    @Override
    public void onPauseWithNative() {
        if (mScreenshotMonitor != null) mScreenshotMonitor.stopMonitoring();
    }

    @Override
    public void onScreenshotTaken() {
        Tab currentTab = mCurrentTabSupplier.get();
        Profile currentProfile = currentTab != null ? currentTab.getProfile() : mProfile;

        Tracker tracker = TrackerFactory.getTrackerForProfile(currentProfile);
        tracker.notifyEvent(EventConstants.SCREENSHOT_TAKEN_CHROME_IN_FOREGROUND);

        if (currentTab == null) return;

        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    if (currentTab != mCurrentTabSupplier.get()) return;
                    showDownloadPageTextBubble(
                            currentTab, FeatureConstants.DOWNLOAD_PAGE_SCREENSHOT_FEATURE);
                    ScreenshotTabObserver tabObserver = ScreenshotTabObserver.from(currentTab);
                    if (tabObserver != null) tabObserver.onScreenshotTaken();
                });
    }

    // Private methods.
    private static int getDataReductionMenuItemHighlight() {
        return R.id.app_menu_footer;
    }

    private void showDownloadHomeIPH() {
        mUserEducationHelper.requestShowIPH(
                new IPHCommandBuilder(
                                mActivity.getResources(),
                                FeatureConstants.DOWNLOAD_HOME_FEATURE,
                                R.string.iph_download_home_text,
                                R.string.iph_download_home_accessibility_text)
                        .setAnchorView(mMenuButtonAnchorView)
                        .setOnShowCallback(() -> turnOnHighlightForMenuItem(R.id.downloads_menu_id))
                        .setOnDismissCallback(this::turnOffHighlightForMenuItem)
                        .build());
    }

    /**
     * Show the download page in-product-help bubble. Also used by download page screenshot IPH.
     * @param tab The current tab.
     */
    private void showDownloadPageTextBubble(final Tab tab, String featureName) {
        if (tab == null) return;
        if (DeviceFormFactor.isWindowOnTablet(mWindowAndroid)
                || (mIsInOverviewModeSupplier.get() != null && mIsInOverviewModeSupplier.get())
                || !DownloadUtils.isAllowedToDownloadPage(tab)) {
            return;
        }

        mUserEducationHelper.requestShowIPH(
                new IPHCommandBuilder(
                                mActivity.getResources(),
                                featureName,
                                R.string.iph_download_page_for_offline_usage_text,
                                R.string.iph_download_page_for_offline_usage_accessibility_text)
                        .setOnShowCallback(() -> turnOnHighlightForMenuItem(R.id.offline_page_id))
                        .setOnDismissCallback(this::turnOffHighlightForMenuItem)
                        .setAnchorView(mMenuButtonAnchorView)
                        .build());
        // Record metrics if we show Download IPH after a screenshot of the page.
        ScreenshotTabObserver tabObserver = ScreenshotTabObserver.from(tab);
        if (tabObserver != null) {
            tabObserver.onActionPerformedAfterScreenshot(
                    ScreenshotTabObserver.SCREENSHOT_ACTION_DOWNLOAD_IPH);
        }
    }

    /**
     * Show the translate manual trigger in-product-help bubble.
     * @param tab The current tab.
     */
    private void showTranslateMenuButtonTextBubble(final Tab tab) {
        if (tab == null) return;
        if (!TranslateUtils.canTranslateCurrentTab(tab)
                || !TranslateBridge.shouldShowManualTranslateIPH(tab)) {
            return;
        }

        mUserEducationHelper.requestShowIPH(
                new IPHCommandBuilder(
                                mActivity.getResources(),
                                FeatureConstants.TRANSLATE_MENU_BUTTON_FEATURE,
                                R.string.iph_translate_menu_button_text,
                                R.string.iph_translate_menu_button_accessibility_text)
                        .setOnShowCallback(() -> turnOnHighlightForMenuItem(R.id.translate_id))
                        .setOnDismissCallback(this::turnOffHighlightForMenuItem)
                        .setAnchorView(mMenuButtonAnchorView)
                        .build());
    }

    private void turnOnHighlightForMenuItem(Integer highlightMenuItemId) {
        if (mAppMenuHandler != null) {
            mAppMenuHandler.setMenuHighlight(highlightMenuItemId);
        }
    }

    private void turnOffHighlightForMenuItem() {
        if (mAppMenuHandler != null) {
            mAppMenuHandler.clearMenuHighlight();
        }
    }
}