chromium/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleUtils.java

// Copyright 2022 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.search_resumption;

import android.text.TextUtils;
import android.view.ViewGroup;

import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.search_resumption.SearchResumptionUserData.SuggestionResult;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.url.GURL;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.TimeUnit;

/** This is a utility class for search resumption module. */
public class SearchResumptionModuleUtils {
    @IntDef({ModuleShowStatus.EXPANDED, ModuleShowStatus.COLLAPSED, ModuleShowStatus.NUM_ENTRIES})
    @Retention(RetentionPolicy.SOURCE)
    // The ModuleShowStatus should be consistent with SearchResumptionModule.ModuleShowStatus in
    // enums.xml.
    public @interface ModuleShowStatus {
        int EXPANDED = 0;
        int COLLAPSED = 1;
        int NUM_ENTRIES = 2;
    }

    @IntDef({
        ModuleNotShownReason.NOT_ENOUGH_RESULT,
        ModuleNotShownReason.FEATURE_DISABLED,
        ModuleNotShownReason.NOT_SIGN_IN,
        ModuleNotShownReason.NOT_SYNC,
        ModuleNotShownReason.DEFAULT_ENGINE_NOT_GOOGLE,
        ModuleNotShownReason.NO_TAB_TO_TRACK,
        ModuleNotShownReason.TAB_NOT_VALID,
        ModuleNotShownReason.TAB_EXPIRED,
        ModuleNotShownReason.TAB_CHANGED,
        ModuleNotShownReason.NUM_ENTRIES
    })
    // The ModuleNotShownReason should be consistent with
    // SearchResumptionModule.ModuleNotShownReason in enums.xml.
    public @interface ModuleNotShownReason {
        int NOT_ENOUGH_RESULT = 0;
        int FEATURE_DISABLED = 1;
        int NOT_SIGN_IN = 2;
        int NOT_SYNC = 3;
        int DEFAULT_ENGINE_NOT_GOOGLE = 4;
        int NO_TAB_TO_TRACK = 5;
        int TAB_NOT_VALID = 6;
        int TAB_EXPIRED = 7;
        int TAB_CHANGED = 8;
        int NUM_ENTRIES = 9;
    }

    @VisibleForTesting
    static final String UMA_MODULE_SHOW = "NewTabPage.SearchResumptionModule.Show";

    @VisibleForTesting
    static final String UMA_MODULE_NOT_SHOW = "NewTabPage.SearchResumptionModule.NotShow";

    static final String UMA_MODULE_SHOW_CACHED = "NewTabPage.SearchResumptionModule.Show.Cached";
    static final String TAB_EXPIRATION_TIME_PARAM = "tab_expiration_time";
    static final String ACTION_CLICK = "SearchResumptionModule.NTP.Click";
    static final String ACTION_COLLAPSE = "SearchResumptionModule.NTP.Collapse";
    static final String ACTION_EXPAND = "SearchResumptionModule.NTP.Expand";
    static final String USE_NEW_SERVICE_PARAM = "use_new_service";
    static final int LAST_TAB_EXPIRATION_TIME_SECONDS = 3600; // 1 Hour

    /**
     * Creates a {@link SearchResumptionModuleCoordinator} if we are currently allowed to and
     * dependencies are met:
     *
     * <ol>
     *   <li>Feature flag SEARCH_RESUMPTION_MODULE_ANDROID is enabled;
     *   <li>The default search engine is Google;
     *   <li>The user has signed in;
     *   <li>The current Tab isn't shown due to such as tapping the back button.
     *   <li>The Tab to track is a regular Tab, not a native page, not with an empty URL;
     *   <li>The Tab to track was visited within an expiration time.
     * </ol>
     *
     * @param parent The parent layout which the search resumption module lives.
     * @param tabModel The TabModel to find the Tab to track.
     * @param currentTab The Tab that the search resumption module is associated to.
     * @param profile The profile of the user.
     * @param moduleContainerStubId The id of the search resumption module on its parent view.
     */
    public static SearchResumptionModuleCoordinator mayCreateSearchResumptionModule(
            ViewGroup parent,
            TabModel tabModel,
            Tab currentTab,
            Profile profile,
            int moduleContainerStubId) {
        if (!shouldShowSearchResumptionModule(profile)) return null;

        Tab tabToTrack = TabModelUtils.getMostRecentTab(tabModel, currentTab.getId());
        if (tabToTrack == null) {
            recordModuleNotShownReason(ModuleNotShownReason.NO_TAB_TO_TRACK);
            return null;
        }

        if (!isTabToTrackValid(tabToTrack)) return null;

        return new SearchResumptionModuleCoordinator(
                parent,
                tabToTrack,
                currentTab,
                profile,
                moduleContainerStubId,
                mayGetCachedResults(currentTab, tabToTrack));
    }

    /**
     * Returns whether to show the search resumption module. Only shows the module if all of the
     * criteria meet:
     *
     * <ol>
     *   <li>Feature flag SEARCH_RESUMPTION_MODULE_ANDROID is enabled;
     *   <li>The default search engine is Google;
     *   <li>The user has signed in;
     *   <li>The user has turned on sync.
     * </ol>
     */
    @VisibleForTesting
    static boolean shouldShowSearchResumptionModule(Profile profile) {
        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.SEARCH_RESUMPTION_MODULE_ANDROID)) {
            recordModuleNotShownReason(ModuleNotShownReason.FEATURE_DISABLED);
            return false;
        }

        if (!TemplateUrlServiceFactory.getForProfile(profile).isDefaultSearchEngineGoogle()) {
            recordModuleNotShownReason(ModuleNotShownReason.DEFAULT_ENGINE_NOT_GOOGLE);
            return false;
        }

        if (!IdentityServicesProvider.get()
                .getIdentityManager(profile)
                .hasPrimaryAccount(ConsentLevel.SIGNIN)) {
            recordModuleNotShownReason(ModuleNotShownReason.NOT_SIGN_IN);
            return false;
        }

        if (!SyncServiceFactory.getForProfile(profile)
                .getSelectedTypes()
                .contains(UserSelectableType.HISTORY)) {
            recordModuleNotShownReason(ModuleNotShownReason.NOT_SYNC);
            return false;
        }
        return true;
    }

    /**
     * Returns whether the Tab to track is valid for providing search suggestions:
     * 1) The Tab to track is a regular Tab, not a native page, not with an empty URL;
     * 2) The Tab to track was visited within an expiration time.
     */
    @VisibleForTesting
    static boolean isTabToTrackValid(Tab tabToTrack) {
        if (tabToTrack.isNativePage()
                || tabToTrack.isIncognito()
                || GURL.isEmptyOrInvalid(tabToTrack.getUrl())) {
            recordModuleNotShownReason(ModuleNotShownReason.TAB_NOT_VALID);
            return false;
        }

        // Only shows the module if the Tab to track was visited within an expiration time.
        if (TimeUnit.MILLISECONDS.toSeconds(
                        System.currentTimeMillis() - tabToTrack.getTimestampMillis())
                < ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
                        ChromeFeatureList.SEARCH_RESUMPTION_MODULE_ANDROID,
                        TAB_EXPIRATION_TIME_PARAM,
                        LAST_TAB_EXPIRATION_TIME_SECONDS)) {
            return true;
        } else {
            recordModuleNotShownReason(ModuleNotShownReason.TAB_EXPIRED);
            return false;
        }
    }

    @VisibleForTesting
    static SuggestionResult mayGetCachedResults(Tab currentTab, Tab tabToTrack) {
        SuggestionResult cachedSuggestions = null;
        if (currentTab.canGoForward()) {
            // If the NTP is created due to any back operation, i.e., its Tab has navigated before,
            // only show the search resumption module if it has been shown before and the last
            // visited Tab hasn't changed. This prevents showing cached suggestions if the previous
            // tracking Tab has been deleted.
            cachedSuggestions =
                    SearchResumptionUserData.getInstance().getCachedSuggestions(currentTab);
            if (cachedSuggestions == null
                    || !TextUtils.equals(
                            cachedSuggestions.getLastUrlToTrack().getSpec(),
                            tabToTrack.getUrl().getSpec())) {
                SearchResumptionModuleUtils.recordModuleNotShownReason(
                        ModuleNotShownReason.TAB_CHANGED);
                return null;
            }
        }
        return cachedSuggestions;
    }

    /**
     * Records histogram when the search resumption module is shown.
     * @param cached: Whether cached suggestions are shown.
     */
    static void recordModuleShown(boolean cached) {
        boolean isCollapsed =
                ChromeSharedPreferences.getInstance()
                        .readBoolean(
                                ChromePreferenceKeys.SEARCH_RESUMPTION_MODULE_COLLAPSE_ON_NTP,
                                false);
        RecordHistogram.recordEnumeratedHistogram(
                cached ? UMA_MODULE_SHOW_CACHED : UMA_MODULE_SHOW,
                isCollapsed ? ModuleShowStatus.COLLAPSED : ModuleShowStatus.EXPANDED,
                ModuleShowStatus.NUM_ENTRIES);
    }

    /** Records the reason why the search resumption module is not shown. */
    static void recordModuleNotShownReason(@ModuleNotShownReason int reason) {
        RecordHistogram.recordEnumeratedHistogram(
                UMA_MODULE_NOT_SHOW, reason, ModuleNotShownReason.NUM_ENTRIES);
    }
}