chromium/chrome/android/java/src/org/chromium/chrome/browser/sync/settings/GoogleServicesSettings.java

// Copyright 2020 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.sync.settings;

import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.commerce.ShoppingFeatures;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchFieldTrial;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.metrics.ChangeMetricsReportingStateCalledFrom;
import org.chromium.chrome.browser.metrics.UmaSessionStats;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridge;
import org.chromium.chrome.browser.password_manager.account_storage_toggle.AccountStorageToggleFragmentArgs;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.price_tracking.PriceTrackingUtilities;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.settings.ChromeBaseSettingsFragment;
import org.chromium.chrome.browser.settings.ChromeManagedPreferenceDelegate;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.ProfileDataCache;
import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.signin.SignOutCoordinator;
import org.chromium.chrome.browser.ui.theme.ChromeSemanticColorUtils;
import org.chromium.chrome.browser.usage_stats.UsageStatsConsentDialog;
import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
import org.chromium.components.browser_ui.settings.ManagedPreferenceDelegate;
import org.chromium.components.browser_ui.settings.SettingsUtils;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.signin.metrics.SignoutReason;
import org.chromium.components.sync.SyncService;
import org.chromium.components.sync.SyncService.SyncStateChangedListener;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.ui.modaldialog.ModalDialogManagerHolder;

/**
 * Settings fragment controlling a number of features communicating with Google services, such as
 * search autocomplete and the automatic upload of crash reports.
 */
public class GoogleServicesSettings extends ChromeBaseSettingsFragment
        implements Preference.OnPreferenceChangeListener,
                SyncStateChangedListener,
                ProfileDataCache.Observer {
    // No longer used. Do not delete. Do not reuse these same strings.
    // private static final String SIGN_OUT_DIALOG_TAG = "sign_out_dialog_tag";
    // public static final String PREF_AUTOFILL_ASSISTANT = "autofill_assistant";
    // public static final String PREF_AUTOFILL_ASSISTANT_SUBSECTION =
    // "autofill_assistant_subsection";

    @VisibleForTesting public static final String PREF_ALLOW_SIGNIN = "allow_signin";

    @VisibleForTesting
    public static final String PREF_PASSWORDS_ACCOUNT_STORAGE = "passwords_account_storage";

    private static final String PREF_SEARCH_SUGGESTIONS = "search_suggestions";
    private static final String PREF_USAGE_AND_CRASH_REPORTING = "usage_and_crash_reports";
    private static final String PREF_URL_KEYED_ANONYMIZED_DATA = "url_keyed_anonymized_data";
    private static final String PREF_CONTEXTUAL_SEARCH = "contextual_search";

    @VisibleForTesting
    public static final String PREF_USAGE_STATS_REPORTING = "usage_stats_reporting";
    @VisibleForTesting
    public static final String PREF_PRICE_TRACKING_ANNOTATIONS = "price_tracking_annotations";

    private static final String PREF_PRICE_NOTIFICATION_SECTION = "price_notifications_section";

    private final PrivacyPreferencesManagerImpl mPrivacyPrefManager =
            PrivacyPreferencesManagerImpl.getInstance();

    private ManagedPreferenceDelegate mManagedPreferenceDelegate;
    private PrefService mPrefService;
    private ProfileDataCache mProfileDataCache;

    private ChromeSwitchPreference mAllowSignin;
    private ChromeSwitchPreference mPasswordsAccountStorage;
    private ChromeSwitchPreference mSearchSuggestions;
    private ChromeSwitchPreference mUsageAndCrashReporting;
    private ChromeSwitchPreference mUrlKeyedAnonymizedData;
    private ChromeSwitchPreference mPriceTrackingAnnotations;
    private @Nullable Preference mContextualSearch;
    private Preference mPriceNotificationSection;
    private Preference mUsageStatsReporting;
    private OneshotSupplier<SnackbarManager> mSnackbarManagerSupplier;
    private final ObservableSupplierImpl<String> mPageTitle = new ObservableSupplierImpl<>();

    @Override
    public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
        mPageTitle.set(getString(R.string.prefs_google_services));
        setHasOptionsMenu(true);

        mProfileDataCache = ProfileDataCache.createWithDefaultImageSizeAndNoBadge(getActivity());
        mProfileDataCache.addObserver(this);
        mPrefService = UserPrefs.get(getProfile());
        mManagedPreferenceDelegate = createManagedPreferenceDelegate();

        SettingsUtils.addPreferencesFromResource(this, R.xml.google_services_preferences);

        mAllowSignin = (ChromeSwitchPreference) findPreference(PREF_ALLOW_SIGNIN);

        if (getProfile().isChild()) {
            // Do not display option to allow / disallow sign-in for supervised accounts since
            // these require the user to be signed-in and syncing.
            mAllowSignin.setVisible(false);
        } else {
            mAllowSignin.setOnPreferenceChangeListener(this);
            mAllowSignin.setManagedPreferenceDelegate(mManagedPreferenceDelegate);
        }

        mPasswordsAccountStorage =
                (ChromeSwitchPreference) findPreference(PREF_PASSWORDS_ACCOUNT_STORAGE);
        mPasswordsAccountStorage.setOnPreferenceChangeListener(this);
        if (getArguments() != null
                && getArguments().getBoolean(AccountStorageToggleFragmentArgs.HIGHLIGHT)) {
            mPasswordsAccountStorage.setBackgroundColor(
                    ChromeSemanticColorUtils.getIphHighlightColor(getContext()));
        }
        SyncServiceFactory.getForProfile(getProfile()).addSyncStateChangedListener(this);

        mSearchSuggestions = (ChromeSwitchPreference) findPreference(PREF_SEARCH_SUGGESTIONS);
        mSearchSuggestions.setOnPreferenceChangeListener(this);
        mSearchSuggestions.setManagedPreferenceDelegate(mManagedPreferenceDelegate);

        mUsageAndCrashReporting =
                (ChromeSwitchPreference) findPreference(PREF_USAGE_AND_CRASH_REPORTING);
        mUsageAndCrashReporting.setOnPreferenceChangeListener(this);
        mUsageAndCrashReporting.setManagedPreferenceDelegate(mManagedPreferenceDelegate);

        mUrlKeyedAnonymizedData =
                (ChromeSwitchPreference) findPreference(PREF_URL_KEYED_ANONYMIZED_DATA);
        mUrlKeyedAnonymizedData.setOnPreferenceChangeListener(this);
        mUrlKeyedAnonymizedData.setManagedPreferenceDelegate(mManagedPreferenceDelegate);

        mContextualSearch = findPreference(PREF_CONTEXTUAL_SEARCH);
        if (!ContextualSearchFieldTrial.isEnabled()) {
            removePreference(getPreferenceScreen(), mContextualSearch);
            mContextualSearch = null;
        }

        mPriceTrackingAnnotations =
                (ChromeSwitchPreference) findPreference(PREF_PRICE_TRACKING_ANNOTATIONS);
        if (!PriceTrackingFeatures.allowUsersToDisablePriceAnnotations(getProfile())) {
            removePreference(getPreferenceScreen(), mPriceTrackingAnnotations);
            mPriceTrackingAnnotations = null;
        } else {
            mPriceTrackingAnnotations.setOnPreferenceChangeListener(this);
            mPriceTrackingAnnotations.setManagedPreferenceDelegate(mManagedPreferenceDelegate);
        }

        mPriceNotificationSection = findPreference(PREF_PRICE_NOTIFICATION_SECTION);
        if (ShoppingFeatures.isShoppingListEligible(getProfile())) {
            mPriceNotificationSection.setVisible(true);
        } else {
            removePreference(getPreferenceScreen(), mPriceNotificationSection);
            mPriceNotificationSection = null;
        }

        mUsageStatsReporting = findPreference(PREF_USAGE_STATS_REPORTING);
        mUsageStatsReporting.setVisible(true);

        updatePreferences();
    }

    @Override
    public ObservableSupplier<String> getPageTitle() {
        return mPageTitle;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        SyncServiceFactory.getForProfile(getProfile()).removeSyncStateChangedListener(this);
        mProfileDataCache.removeObserver(this);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        menu.clear();
        MenuItem help =
                menu.add(Menu.NONE, R.id.menu_id_targeted_help, Menu.NONE, R.string.menu_help);
        help.setIcon(R.drawable.ic_help_and_feedback);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.menu_id_targeted_help) {
            getHelpAndFeedbackLauncher()
                    .show(getActivity(), getString(R.string.help_context_sync_and_services), null);
            return true;
        }
        return false;
    }

    @Override
    public void onResume() {
        super.onResume();
        updatePreferences();
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        String key = preference.getKey();
        if (PREF_ALLOW_SIGNIN.equals(key)) {
            assert !getProfile().isChild() : "A supervised account must not update allow sign-in.";

            IdentityManager identityManager =
                    IdentityServicesProvider.get().getIdentityManager(getProfile());
            boolean shouldSignUserOut =
                    identityManager.hasPrimaryAccount(ConsentLevel.SIGNIN) && !((boolean) newValue);
            if (!shouldSignUserOut) {
                mPrefService.setBoolean(Pref.SIGNIN_ALLOWED, (boolean) newValue);
                return true;
            }

            if (!ChromeFeatureList.isEnabled(
                            ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
                    && !identityManager.hasPrimaryAccount(ConsentLevel.SYNC)) {
                // Don't show signout dialog if there's no sync consent, as it never wipes the data.
                IdentityServicesProvider.get()
                        .getSigninManager(getProfile())
                        .signOut(SignoutReason.USER_DISABLED_ALLOW_CHROME_SIGN_IN, null, false);
                mPrefService.setBoolean(Pref.SIGNIN_ALLOWED, false);
                return true;
            }

            // TODO(crbug.com/350699437): Use a different SignoutReason.
            SignOutCoordinator.startSignOutFlow(
                    requireContext(),
                    getProfile(),
                    getChildFragmentManager(),
                    ((ModalDialogManagerHolder) getActivity()).getModalDialogManager(),
                    mSnackbarManagerSupplier.get(),
                    SignoutReason.USER_DISABLED_ALLOW_CHROME_SIGN_IN,
                    /* showConfirmDialog= */ true,
                    () -> {
                        mPrefService.setBoolean(Pref.SIGNIN_ALLOWED, false);
                        updatePreferences();
                    });
            // Don't change the preference state yet, it will be updated by SignOutCoordinator if
            // the user actually confirms the sign-out.
            return false;
        } else if (PREF_PASSWORDS_ACCOUNT_STORAGE.equals(key)) {
            SyncServiceFactory.getForProfile(getProfile())
                    .setSelectedType(UserSelectableType.PASSWORDS, (boolean) newValue);
        } else if (PREF_SEARCH_SUGGESTIONS.equals(key)) {
            mPrefService.setBoolean(Pref.SEARCH_SUGGEST_ENABLED, (boolean) newValue);
        } else if (PREF_USAGE_AND_CRASH_REPORTING.equals(key)) {
            UmaSessionStats.changeMetricsReportingConsent(
                    (boolean) newValue, ChangeMetricsReportingStateCalledFrom.UI_SETTINGS);
        } else if (PREF_URL_KEYED_ANONYMIZED_DATA.equals(key)) {
            UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(
                    getProfile(), (boolean) newValue);
        } else if (PREF_PRICE_TRACKING_ANNOTATIONS.equals(key)) {
            PriceTrackingUtilities.setTrackPricesOnTabsEnabled((boolean) newValue);
        }
        return true;
    }

    public void setSnackbarManagerSupplier(
            OneshotSupplier<SnackbarManager> snackbarManagerSupplier) {
        mSnackbarManagerSupplier = snackbarManagerSupplier;
    }

    // SyncStateChangedListener overrides.
    @Override
    public void syncStateChanged() {
        updatePasswordsAccountStoragePreference();
    }

    // ProfileDataCache.Observer overrides.
    @Override
    public void onProfileDataUpdated(String accountEmail) {
        updatePasswordsAccountStoragePreference();
    }

    private static void removePreference(PreferenceGroup from, Preference preference) {
        boolean found = from.removePreference(preference);
        assert found : "Don't have such preference! Preference key: " + preference.getKey();
    }

    private void updatePreferences() {
        mAllowSignin.setChecked(mPrefService.getBoolean(Pref.SIGNIN_ALLOWED));
        updatePasswordsAccountStoragePreference();
        mSearchSuggestions.setChecked(mPrefService.getBoolean(Pref.SEARCH_SUGGEST_ENABLED));
        mUsageAndCrashReporting.setChecked(mPrivacyPrefManager.isUsageAndCrashReportingPermitted());
        mUrlKeyedAnonymizedData.setChecked(
                UnifiedConsentServiceBridge.isUrlKeyedAnonymizedDataCollectionEnabled(
                        getProfile()));

        if (mContextualSearch != null) {
            boolean isContextualSearchEnabled =
                    !ContextualSearchManager.isContextualSearchDisabled(getProfile());
            mContextualSearch.setSummary(
                    isContextualSearchEnabled ? R.string.text_on : R.string.text_off);
        }
        if (mPriceTrackingAnnotations != null) {
            mPriceTrackingAnnotations.setChecked(
                    PriceTrackingUtilities.isTrackPricesOnTabsEnabled(getProfile()));
        }
        if (mUsageStatsReporting != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                    && mPrefService.getBoolean(Pref.USAGE_STATS_ENABLED)) {
                mUsageStatsReporting.setOnPreferenceClickListener(
                        preference -> {
                            UsageStatsConsentDialog.create(
                                            getActivity(),
                                            getProfile(),
                                            true,
                                            (didConfirm) -> {
                                                if (didConfirm) {
                                                    updatePreferences();
                                                }
                                            })
                                    .show();
                            return true;
                        });
            } else {
                removePreference(getPreferenceScreen(), mUsageStatsReporting);
                mUsageStatsReporting = null;
            }
        }
    }

    private void updatePasswordsAccountStoragePreference() {
        SyncService syncService = SyncServiceFactory.getForProfile(getProfile());
        mPasswordsAccountStorage.setChecked(
                syncService.getSelectedTypes().contains(UserSelectableType.PASSWORDS));
        CoreAccountInfo account = syncService.getAccountInfo();
        mPasswordsAccountStorage.setVisible(
                syncService.getAccountInfo() != null
                        && !syncService.hasSyncConsent()
                        && !PasswordManagerUtilBridge.isGmsCoreUpdateRequired(
                                UserPrefs.get(getProfile()), syncService)
                        && ChromeFeatureList.isEnabled(
                                ChromeFeatureList
                                        .ENABLE_PASSWORDS_ACCOUNT_STORAGE_FOR_NON_SYNCING_USERS)
                        && !ChromeFeatureList.isEnabled(
                                ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS));
        if (account == null) {
            // The toggle is not visible, no need to set a summary.
            return;
        }
        boolean canDisplayEmail =
                mProfileDataCache
                        .getProfileDataOrDefault(account.getEmail())
                        .hasDisplayableEmailAddress();
        mPasswordsAccountStorage.setSummary(
                canDisplayEmail
                        ? getString(
                                R.string.passwords_account_storage_toggle_summary,
                                syncService.getAccountInfo().getEmail())
                        : getString(R.string.passwords_account_storage_toggle_summary_no_email));
    }

    private ChromeManagedPreferenceDelegate createManagedPreferenceDelegate() {
        return new ChromeManagedPreferenceDelegate(getProfile()) {
            @Override
            public boolean isPreferenceControlledByPolicy(Preference preference) {
                String key = preference.getKey();
                if (PREF_ALLOW_SIGNIN.equals(key)) {
                    return mPrefService.isManagedPreference(Pref.SIGNIN_ALLOWED);
                }
                if (PREF_PASSWORDS_ACCOUNT_STORAGE.equals(key)) {
                    return SyncServiceFactory.getForProfile(getProfile())
                            .isTypeManagedByPolicy(UserSelectableType.PASSWORDS);
                }
                if (PREF_SEARCH_SUGGESTIONS.equals(key)) {
                    return mPrefService.isManagedPreference(Pref.SEARCH_SUGGEST_ENABLED);
                }
                if (PREF_USAGE_AND_CRASH_REPORTING.equals(key)) {
                    return !PrivacyPreferencesManagerImpl.getInstance()
                            .isUsageAndCrashReportingPermittedByPolicy();
                }
                if (PREF_URL_KEYED_ANONYMIZED_DATA.equals(key)) {
                    return UnifiedConsentServiceBridge.isUrlKeyedAnonymizedDataCollectionManaged(
                            getProfile());
                }
                return false;
            }
        };
    }
}