chromium/chrome/android/java/src/org/chromium/chrome/browser/gesturenav/RtlGestureNavIphController.java

// Copyright 2024 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.gesturenav;

import android.view.ViewGroup;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.lifetime.Destroyable;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
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_public.browser.NavigationHandle;

/**
 * A controller triggering the Iph Dialog when conditions are satisfied.
 *
 * <p>First arm: when both back stack and forward stack are not empty.
 *
 * <p>Second arm: when user tries to navigate back/forward but there is nothing in the corresponding
 * history stack; i.e. fail to navigate back/forward because of empty stack.
 */
public class RtlGestureNavIphController implements Destroyable {

    private class RtlGestureNavTabObserver extends ActivityTabTabObserver {
        public RtlGestureNavTabObserver(ActivityTabProvider tabProvider) {
            super(tabProvider);
        }

        @Override
        public void onDidFinishNavigationInPrimaryMainFrame(
                Tab tab, NavigationHandle navigationHandle) {
            onNavStateChanged(tab);
        }
    }

    private static final String UNHANDLED_GESTURE_THRESHOLD_PARAM = "x_unhandled_gesture_threshold";
    private static final int DEFAULT_UNHANDLED_GESTURE_THRESHOLD = 2;
    private static final String TRIGGER_METHOD_PARAM = "x_trigger";
    private static final String TRIGGERED_BY_NON_EMPTY_STACK = "non-empty-stack";

    private @Nullable RtlGestureNavTabObserver mRtlGestureNavTabObserver;
    private final ActivityTabProvider mActivityTabProvider;
    private final Supplier<Profile> mProfileSupplier;
    private int mUnhandledGestureCount;
    private final int mUnhandledGestureThreshold;

    /**
     * @param activityTabProvider The tab provider of providing the current tab.
     */
    public RtlGestureNavIphController(
            ActivityTabProvider activityTabProvider, Supplier<Profile> profileSupplier) {
        mActivityTabProvider = activityTabProvider;
        mProfileSupplier = profileSupplier;
        mUnhandledGestureThreshold =
                ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
                        FeatureConstants.IPH_RTL_GESTURE_NAVIGATION,
                        UNHANDLED_GESTURE_THRESHOLD_PARAM,
                        DEFAULT_UNHANDLED_GESTURE_THRESHOLD);
        if (shouldShowOnNonEmptyStack() && wouldShowIph()) {
            mRtlGestureNavTabObserver = new RtlGestureNavTabObserver(activityTabProvider);
        }
    }

    public void onGestureUnhandled() {
        if (shouldShowOnNonEmptyStack()) return;
        if (mActivityTabProvider.get() == null) return;
        mUnhandledGestureCount++;
        if (mUnhandledGestureCount >= mUnhandledGestureThreshold) {
            Tracker tracker = TrackerFactory.getTrackerForProfile(mProfileSupplier.get());
            if (tracker.shouldTriggerHelpUI(FeatureConstants.IPH_RTL_GESTURE_NAVIGATION)) {
                show();
                mUnhandledGestureCount = 0;
            }
        }
    }

    public void onGestureHandled() {
        mUnhandledGestureCount = 0;
    }

    @Override
    public void destroy() {
        if (mRtlGestureNavTabObserver != null) {
            mRtlGestureNavTabObserver.destroy();
        }
    }

    private void onNavStateChanged(@Nullable Tab tab) {
        if (tab == null) return;
        assert shouldShowOnNonEmptyStack();
        if (tab.canGoBack() || tab.canGoForward()) {
            Tracker tracker = TrackerFactory.getTrackerForProfile(tab.getProfile());
            if (tracker.shouldTriggerHelpUI(FeatureConstants.IPH_RTL_GESTURE_NAVIGATION)) {
                show();
                mRtlGestureNavTabObserver.destroy();
                mRtlGestureNavTabObserver = null;
            }
        }
    }

    private void show() {
        Tab tab = mActivityTabProvider.get();
        RtlGestureNavIphDialog dialog =
                new RtlGestureNavIphDialog(
                        tab.getContext(),
                        tab.getWindowAndroid().getModalDialogManager(),
                        () -> {
                            Tracker tracker =
                                    TrackerFactory.getTrackerForProfile(mProfileSupplier.get());
                            tracker.dismissed(FeatureConstants.IPH_RTL_GESTURE_NAVIGATION);
                        });
        dialog.setParentView((ViewGroup) tab.getView());
        dialog.show();
        Tracker tracker = TrackerFactory.getTrackerForProfile(mProfileSupplier.get());
        tracker.notifyEvent(EventConstants.RTL_GESTURE_NAVIGATION_DIALOG_SHOW);
    }

    private boolean wouldShowIph() {
        Tracker tracker = TrackerFactory.getTrackerForProfile(mProfileSupplier.get());
        return tracker.wouldTriggerHelpUI(FeatureConstants.IPH_RTL_GESTURE_NAVIGATION);
    }

    @VisibleForTesting
    boolean shouldShowOnNonEmptyStack() {
        return ChromeFeatureList.getFieldTrialParamByFeature(
                        FeatureConstants.IPH_RTL_GESTURE_NAVIGATION, TRIGGER_METHOD_PARAM)
                .equals(TRIGGERED_BY_NON_EMPTY_STACK);
    }
}