chromium/chrome/android/java/src/org/chromium/chrome/browser/homepage/HomepageManager.java

// Copyright 2015 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.homepage;

import android.content.Context;

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

import org.chromium.base.ObserverList;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.common.ChromeUrlConstants;
import org.chromium.chrome.browser.homepage.settings.HomepageMetricsEnums.HomepageLocationType;
import org.chromium.chrome.browser.homepage.settings.HomepageSettings;
import org.chromium.chrome.browser.new_tab_url.DseNewTabUrlManager;
import org.chromium.chrome.browser.partnercustomizations.HomepageCharacterizationHelper;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.url.GURL;

/**
 * Provides information regarding homepage enabled states and URI.
 *
 * This class serves as a single homepage logic gateway.
 */
public class HomepageManager
        implements HomepagePolicyManager.HomepagePolicyStateListener,
                PartnerBrowserCustomizations.PartnerHomepageListener {
    /** An interface to use for getting homepage related updates. */
    public interface HomepageStateListener {
        /** Called when the homepage is enabled or disabled or the homepage URL changes. */
        void onHomepageStateUpdated();
    }

    private static HomepageManager sInstance;

    private final SharedPreferencesManager mSharedPreferencesManager;
    private final ObserverList<HomepageStateListener> mHomepageStateListeners;

    private HomepageManager() {
        mSharedPreferencesManager = ChromeSharedPreferences.getInstance();
        mHomepageStateListeners = new ObserverList<>();
        HomepagePolicyManager.getInstance().addListener(this);
        PartnerBrowserCustomizations.getInstance().setPartnerHomepageListener(this);
    }

    /** Returns the singleton instance of HomepageManager, creating it if needed. */
    public static HomepageManager getInstance() {
        if (sInstance == null) {
            sInstance = new HomepageManager();
        }
        return sInstance;
    }

    public static void setInstanceForTesting(HomepageManager homepageManager) {
        HomepageManager prevValue = sInstance;
        sInstance = homepageManager;
        ResettersForTesting.register(() -> sInstance = prevValue);
    }

    /** Adds a HomepageStateListener to receive updates when the homepage state changes. */
    public void addListener(HomepageStateListener listener) {
        mHomepageStateListeners.addObserver(listener);
    }

    /**
     * Removes the given listener from the state listener list.
     * @param listener The listener to remove.
     */
    public void removeListener(HomepageStateListener listener) {
        mHomepageStateListeners.removeObserver(listener);
    }

    /**
     * Menu click handler on home button.
     *
     * @param context {@link Context} used for launching a settings activity.
     */
    public void onMenuClick(Context context) {
        SettingsLauncherFactory.createSettingsLauncher()
                .launchSettingsActivity(context, HomepageSettings.class);
    }

    /** Notify any listeners about a homepage state change. */
    public void notifyHomepageUpdated() {
        for (HomepageStateListener listener : mHomepageStateListeners) {
            listener.onHomepageStateUpdated();
        }
    }

    /**
     * @return Whether or not homepage is enabled.
     */
    public boolean isHomepageEnabled() {
        return HomepagePolicyManager.isHomepageManagedByPolicy() || getPrefHomepageEnabled();
    }

    /**
     * @return Whether to close the app when the user has zero tabs.
     */
    public boolean shouldCloseAppWithZeroTabs() {
        return isHomepageEnabled() && !UrlUtilities.isNtpUrl(getHomepageGurl());
    }

    /**
     * Get the current homepage URI. If the homepage is disabled, return an empty GURL; otherwise it
     * will always return a non-empty GURL. In cases when the homepage is specifically set as empty,
     * this function will fallback to return {@link ChromeUrlConstants.nativeNtpGurl()}. If the
     * default search engine (DSE) isn't Google, may fallback to the DSE's new Tab URL.
     *
     * <p>This function needs to be called on UI thread since
     * ProfileManager.getLastUsedRegularProfile() is called.
     *
     * <p>This function checks different sources to get the current homepage, which is listed below
     * according to their priority:
     *
     * <p><b>isManagedByPolicy > useChromeNtp > useDefaultGurl > useCustomGurl</b>
     *
     * @return A non-empty GURL, if homepage is enabled. An empty GURL otherwise.
     * @see HomepagePolicyManager#isHomepageManagedByPolicy()
     * @see #getPrefHomepageUseChromeNtp()
     * @see #getPrefHomepageUseDefaultUri()
     */
    public @Nullable GURL getHomepageGurl() {
        if (!isHomepageEnabled()) return GURL.emptyGURL();

        GURL homepageGurl = getHomepageGurlIgnoringEnabledState();
        if (homepageGurl.isEmpty()) {
            homepageGurl = ChromeUrlConstants.nativeNtpGurl();
        }

        // We have to use ProfileManager.getLastUsedRegularProfile() to get the last used regular
        // Profile
        // before HomepageManager supports multiple Profiles. Thus, if DSE isn't Google, pressing
        // the home button may redirect to the DSE's new Tab URL, rather than showing an incognito
        // NTP.
        return DseNewTabUrlManager.maybeGetOverrideUrl(
                homepageGurl,
                ProfileManager.isInitialized() ? ProfileManager.getLastUsedRegularProfile() : null);
    }

    /**
     * @return A GURL for the default homepage URI if the homepage is partner provided, or the new
     *     tab page if the homepage button is force enabled via flag.
     */
    public GURL getDefaultHomepageGurl() {
        if (PartnerBrowserCustomizations.getInstance().isHomepageProviderAvailableAndEnabled()) {
            return PartnerBrowserCustomizations.getInstance().getHomePageUrl();
        }

        String homepagePartnerDefaultGurlSerialized =
                ChromeSharedPreferences.getInstance()
                        .readString(
                                ChromePreferenceKeys.HOMEPAGE_PARTNER_CUSTOMIZED_DEFAULT_GURL, "");
        if (!homepagePartnerDefaultGurlSerialized.equals("")) {
            GURL homepagePartnerDefaultGurl =
                    GURL.deserialize(homepagePartnerDefaultGurlSerialized);
            if (!homepagePartnerDefaultGurl.isEmpty()) {
                return homepagePartnerDefaultGurl;
            }
        }

        String homepagePartnerDefaultUri =
                ChromeSharedPreferences.getInstance()
                        .readString(
                                ChromePreferenceKeys
                                        .DEPRECATED_HOMEPAGE_PARTNER_CUSTOMIZED_DEFAULT_URI,
                                "");
        if (!homepagePartnerDefaultUri.equals("")) {
            GURL homepagePartnerDefaultGurl = new GURL(homepagePartnerDefaultUri);
            if (homepagePartnerDefaultGurl.isValid()) {
                ChromeSharedPreferences.getInstance()
                        .writeString(
                                ChromePreferenceKeys.HOMEPAGE_PARTNER_CUSTOMIZED_DEFAULT_GURL,
                                homepagePartnerDefaultGurl.serialize());
                ChromeSharedPreferences.getInstance()
                        .removeKey(
                                ChromePreferenceKeys
                                        .DEPRECATED_HOMEPAGE_PARTNER_CUSTOMIZED_DEFAULT_URI);
                return homepagePartnerDefaultGurl;
            }
        }

        return ChromeUrlConstants.nativeNtpGurl();
    }

    /**
     * Determines whether the homepage is set to something other than the NTP or empty/null.
     * Normally, when loading the homepage the NTP is loaded as a fallback if the homepage is null
     * or empty. So while other helper methods that check if a given string is the NTP will reject
     * null and empty, this method does the opposite.
     *
     * @return Whether the current homepage is something other than the NTP.
     */
    public boolean isHomepageNonNtp() {
        GURL currentHomepage = getHomepageGurl();
        return !currentHomepage.isEmpty() && !UrlUtilities.isNtpUrl(currentHomepage);
    }

    /**
     * Get homepage URI without checking if the homepage is enabled.
     * @return Homepage GURL based on policy and shared preference settings.
     */
    private @NonNull GURL getHomepageGurlIgnoringEnabledState() {
        if (HomepagePolicyManager.isHomepageManagedByPolicy()) {
            return HomepagePolicyManager.getHomepageUrl();
        }
        if (getPrefHomepageUseChromeNtp()) {
            return ChromeUrlConstants.nativeNtpGurl();
        }
        if (getPrefHomepageUseDefaultUri()) {
            return getDefaultHomepageGurl();
        }
        return getPrefHomepageCustomGurl();
    }

    /**
     * Returns the user preference for whether the homepage is enabled. This doesn't take into
     * account whether the device supports having a homepage.
     *
     * @see #isHomepageEnabled
     */
    private boolean getPrefHomepageEnabled() {
        return mSharedPreferencesManager.readBoolean(ChromePreferenceKeys.HOMEPAGE_ENABLED, true);
    }

    /** Sets the user preference for whether the homepage is enabled. */
    public void setPrefHomepageEnabled(boolean enabled) {
        mSharedPreferencesManager.writeBoolean(ChromePreferenceKeys.HOMEPAGE_ENABLED, enabled);
        notifyHomepageUpdated();
    }

    /**
     * @return User specified homepage custom GURL.
     */
    public GURL getPrefHomepageCustomGurl() {
        String homepageCustomGurlSerialized =
                mSharedPreferencesManager.readString(ChromePreferenceKeys.HOMEPAGE_CUSTOM_GURL, "");
        if (!homepageCustomGurlSerialized.equals("")) {
            return GURL.deserialize(homepageCustomGurlSerialized);
        }

        String homepageCustomUri =
                mSharedPreferencesManager.readString(
                        ChromePreferenceKeys.DEPRECATED_HOMEPAGE_CUSTOM_URI, "");
        if (!homepageCustomUri.equals("")) {
            GURL homepageCustomGurl = new GURL(homepageCustomUri);
            if (homepageCustomGurl.isValid()) {
                mSharedPreferencesManager.writeString(
                        ChromePreferenceKeys.HOMEPAGE_CUSTOM_GURL, homepageCustomGurl.serialize());
                mSharedPreferencesManager.removeKey(
                        ChromePreferenceKeys.DEPRECATED_HOMEPAGE_CUSTOM_URI);
                return homepageCustomGurl;
            }
        }

        return GURL.emptyGURL();
    }

    /**
     * True if the homepage URL is the default value. False means the homepage URL is using
     * the user customized URL. Note that this method does not take enterprise policy into account.
     * Use {@link HomepagePolicyManager#isHomepageManagedByPolicy} if policy information is needed.
     *
     * @return Whether if the homepage URL is the default value.
     */
    public boolean getPrefHomepageUseDefaultUri() {
        return mSharedPreferencesManager.readBoolean(
                ChromePreferenceKeys.HOMEPAGE_USE_DEFAULT_URI, true);
    }

    /**
     * @return Whether the homepage is set to Chrome NTP in Homepage settings
     */
    public boolean getPrefHomepageUseChromeNtp() {
        return mSharedPreferencesManager.readBoolean(
                ChromePreferenceKeys.HOMEPAGE_USE_CHROME_NTP, false);
    }

    /**
     * Set homepage related shared preferences, and notify listeners for the homepage status change.
     * These shared preference values will reflect what homepage we are using.
     *
     * <p>The priority of the input pref values during value checking: useChromeNtp > useDefaultGurl
     * > customGurl
     *
     * @param useChromeNtp True if homepage is set as Chrome's New tab page.
     * @param useDefaultGurl True if homepage is using default URI.
     * @param customGurl A GURL for the user customized homepage URI.
     * @see #getHomepageGurl()
     */
    public void setHomepagePreferences(
            boolean useChromeNtp, boolean useDefaultGurl, GURL customGurl) {
        boolean wasUseChromeNtp = getPrefHomepageUseChromeNtp();
        boolean wasUseDefaultUri = getPrefHomepageUseDefaultUri();
        GURL oldCustomGurl = getPrefHomepageCustomGurl();

        if (useChromeNtp == wasUseChromeNtp
                && useDefaultGurl == wasUseDefaultUri
                && oldCustomGurl.equals(customGurl)) {
            return;
        }

        if (useChromeNtp != wasUseChromeNtp) {
            mSharedPreferencesManager.writeBoolean(
                    ChromePreferenceKeys.HOMEPAGE_USE_CHROME_NTP, useChromeNtp);
        }

        if (wasUseDefaultUri != useDefaultGurl) {
            mSharedPreferencesManager.writeBoolean(
                    ChromePreferenceKeys.HOMEPAGE_USE_DEFAULT_URI, useDefaultGurl);
        }

        if (!oldCustomGurl.equals(customGurl)) {
            mSharedPreferencesManager.writeString(
                    ChromePreferenceKeys.HOMEPAGE_CUSTOM_GURL, customGurl.serialize());
        }

        RecordUserAction.record("Settings.Homepage.LocationChanged_V2");
        notifyHomepageUpdated();
    }

    /**
     * Record histogram "Settings.Homepage.LocationType" with the current homepage location type.
     */
    public void recordHomepageLocationTypeIfEnabled() {
        if (!isHomepageEnabled()) return;

        int homepageLocationType = getHomepageLocationType();
        RecordHistogram.recordEnumeratedHistogram(
                "Settings.Homepage.LocationType",
                homepageLocationType,
                HomepageLocationType.NUM_ENTRIES);
    }

    /**
     * @return {@link HomepageLocationType} for current homepage settings.
     */
    @VisibleForTesting
    public @HomepageLocationType int getHomepageLocationType() {
        if (HomepagePolicyManager.isHomepageManagedByPolicy()) {
            return UrlUtilities.isNtpUrl(HomepagePolicyManager.getHomepageUrl())
                    ? HomepageLocationType.POLICY_NTP
                    : HomepageLocationType.POLICY_OTHER;
        }
        if (getPrefHomepageUseChromeNtp()) {
            return HomepageLocationType.USER_CUSTOMIZED_NTP;
        }
        if (getPrefHomepageUseDefaultUri()) {
            if (!PartnerBrowserCustomizations.getInstance()
                    .isHomepageProviderAvailableAndEnabled()) {
                return HomepageLocationType.DEFAULT_NTP;
            }

            return UrlUtilities.isNtpUrl(
                            PartnerBrowserCustomizations.getInstance().getHomePageUrl())
                    ? HomepageLocationType.PARTNER_PROVIDED_NTP
                    : HomepageLocationType.PARTNER_PROVIDED_OTHER;
        }
        // If user type NTP URI as their customized homepage, we'll record user is using NTP.
        return UrlUtilities.isNtpUrl(getPrefHomepageCustomGurl())
                ? HomepageLocationType.USER_CUSTOMIZED_NTP
                : HomepageLocationType.USER_CUSTOMIZED_OTHER;
    }

    @Override
    public void onHomepagePolicyUpdate() {
        notifyHomepageUpdated();
    }

    @Override
    public void onHomepageUpdate() {
        notifyHomepageUpdated();
    }

    /**
     * Provides a helper for UMA reporting of partner customization and Homepage outcomes.
     *
     * @return A {@link HomepageCharacterizationHelper} that provides access to a few helpful
     *     methods.
     */
    public static HomepageCharacterizationHelper getHomepageCharacterizationHelper() {
        return new HomepageCharacterizationHelper() {
            @Override
            public boolean isUrlNtp(@Nullable String url) {
                return UrlConstants.NTP_URL.equals(url) || UrlUtilities.isNtpUrl(url);
            }

            @Override
            public boolean isPartner() {
                switch (getInstance().getHomepageLocationType()) {
                    case HomepageLocationType.PARTNER_PROVIDED_OTHER:
                    case HomepageLocationType.PARTNER_PROVIDED_NTP:
                        return true;
                    default:
                        return false;
                }
            }

            @Override
            public boolean isNtp() {
                switch (getInstance().getHomepageLocationType()) {
                    case HomepageLocationType.POLICY_NTP:
                    case HomepageLocationType.PARTNER_PROVIDED_NTP:
                    case HomepageLocationType.USER_CUSTOMIZED_NTP:
                    case HomepageLocationType.DEFAULT_NTP:
                        return true;
                    default:
                        return false;
                }
            }
        };
    }
}