chromium/chrome/browser/password_manager/android/pwd_migration/java/src/org/chromium/chrome/browser/pwd_migration/PasswordMigrationWarningMediator.java

// Copyright 2023 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.pwd_migration;

import static org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.logPasswordMigrationWarningUserAction;
import static org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.ACCOUNT_DISPLAY_NAME;
import static org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.CURRENT_SCREEN;
import static org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.SHOULD_OFFER_SYNC;
import static org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.VISIBLE;

import android.net.Uri;

import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.password_manager.PasswordManagerBuildflags;
import org.chromium.chrome.browser.password_manager.PasswordMetricsUtil;
import org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.PasswordMigrationWarningSheetStateAtClosing;
import org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.PasswordMigrationWarningUserActions;
import org.chromium.chrome.browser.password_manager.settings.PasswordListObserver;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.MigrationOption;
import org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.ScreenType;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.signin.Tribool;
import org.chromium.components.signin.base.AccountInfo;
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.sync.SyncService;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.ui.modelutil.PropertyModel;

/**
 * Contains the logic for the local passwords migration warning. It sets the state of the model and
 * reacts to events.
 */
class PasswordMigrationWarningMediator
        implements PasswordMigrationWarningOnClickHandler, PasswordListObserver {
    private PropertyModel mModel;
    private Profile mProfile;
    private MigrationWarningOptionsHandler mOptionsHandler;
    private @PasswordMigrationWarningTriggers int mReferrer;

    public interface MigrationWarningOptionsHandler {
        /** Launches the sync consent flow. */
        void startSyncConsentFlow();

        /** Opens the sync settings to allow users to enable passwords sync. */
        void openSyncSettings();

        /**
         * Launches the password export flow.
         *
         * @param fragmentManager for the fragment that owns the export flow.
         */
        void startExportFlow(FragmentManager fragmentManager);

        /**
         * Writes the passwords into the file.
         *
         * @param passwordsFile The file into which the passwords are expected to be saved.
         */
        void savePasswordsToDownloads(Uri passwordsFile);

        /** Resumes the password export flow. */
        void resumeExportFlow();

        /** Notifies the {@link ExportFlow} that passwords are fetched. */
        void passwordsAvailable();
    }

    PasswordMigrationWarningMediator(
            Profile profile,
            MigrationWarningOptionsHandler optionsHandler,
            @PasswordMigrationWarningTriggers int referrer) {
        mProfile = profile;
        mOptionsHandler = optionsHandler;
        mReferrer = referrer;
    }

    void initializeModel(PropertyModel model) {
        mModel = model;
    }

    void showWarning(@ScreenType int screenType) {
        mModel.set(SHOULD_OFFER_SYNC, shouldOfferSync());
        mModel.set(VISIBLE, true);
        mModel.set(CURRENT_SCREEN, screenType);
        mModel.set(ACCOUNT_DISPLAY_NAME, getAccountDisplayName(mProfile));
    }

    void onShown() {
        if (mReferrer != PasswordMigrationWarningTriggers.CHROME_STARTUP) {
            return;
        }
        PrefService prefService = UserPrefs.get(mProfile);
        prefService.setBoolean(Pref.LOCAL_PASSWORD_MIGRATION_WARNING_SHOWN_AT_STARTUP, true);
    }

    /**
     * Called when BottomSheetObserver.onSheetClosed is invoked.
     * This is not the same as onDismissed, which is called in both onSheetClosed and in
     * onSheetStateChanged. This is because sometimes, if the BottomSheet was interrupted,
     * onSheetClosed won't get called.
     *
     * @param reason is the cause for the sheet to change its state.
     * @param setFragmentWasCalled indicates that the PasswordMigrationWarningView.setFragment()
     *         method was called.
     */
    void onSheetClosed(
            @BottomSheetController.StateChangeReason int reason, boolean setFragmentWasCalled) {
        if (!setFragmentWasCalled) {
            recordEmptySheetTrigger(mReferrer);
            resetTimestamp();
        }
        recordSheetStateAtClosing(reason, setFragmentWasCalled);
    }

    void onDismissed(@StateChangeReason int reason) {
        if (!mModel.get(VISIBLE)) return; // Dismiss only if not dismissed yet.
        mModel.set(VISIBLE, false);

        if (reason == StateChangeReason.SWIPE
                || reason == StateChangeReason.BACK_PRESS
                || reason == StateChangeReason.TAP_SCRIM
                || reason == StateChangeReason.OMNIBOX_FOCUS) {
            int dismissalKind =
                    mModel.get(CURRENT_SCREEN) == ScreenType.INTRO_SCREEN
                            ? PasswordMigrationWarningUserActions.DISMISS_INTRODUCTION
                            : PasswordMigrationWarningUserActions.DISMISS_MORE_OPTIONS;

            logPasswordMigrationWarningUserAction(dismissalKind);
        }
    }

    @Override
    public void onAcknowledge(BottomSheetController bottomSheetController) {
        mModel.set(VISIBLE, false);

        PrefService prefService = UserPrefs.get(mProfile);
        prefService.setBoolean(Pref.USER_ACKNOWLEDGED_LOCAL_PASSWORDS_MIGRATION_WARNING, true);

        logPasswordMigrationWarningUserAction(PasswordMigrationWarningUserActions.GOT_IT);
    }

    @Override
    public void onMoreOptions() {
        assert mModel.get(VISIBLE);
        mModel.set(CURRENT_SCREEN, ScreenType.OPTIONS_SCREEN);

        logPasswordMigrationWarningUserAction(PasswordMigrationWarningUserActions.MORE_OPTIONS);
    }

    @Override
    public void onNext(@MigrationOption int selectedOption, FragmentManager fragmentManager) {
        if (selectedOption == MigrationOption.SYNC_PASSWORDS) {
            mModel.set(VISIBLE, false);
            startSyncFlow();

            logPasswordMigrationWarningUserAction(PasswordMigrationWarningUserActions.SYNC);
        } else {
            mOptionsHandler.startExportFlow(fragmentManager);

            logPasswordMigrationWarningUserAction(PasswordMigrationWarningUserActions.EXPORT);
        }
    }

    @Override
    public void onSavePasswordsToDownloads(Uri passwordsFile) {
        mOptionsHandler.savePasswordsToDownloads(passwordsFile);
    }

    @Override
    public void onCancel(BottomSheetController bottomSheetController) {
        mModel.set(VISIBLE, false);

        logPasswordMigrationWarningUserAction(PasswordMigrationWarningUserActions.CANCEL);
    }

    private @Nullable String getAccountDisplayName(Profile profile) {
        IdentityManager identityManager =
                IdentityServicesProvider.get().getIdentityManager(profile);
        CoreAccountInfo coreAccountInfo =
                identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN);
        if (coreAccountInfo == null || coreAccountInfo.getEmail().isEmpty()) {
            return null;
        }
        @Nullable
        AccountInfo account =
                identityManager.findExtendedAccountInfoByEmailAddress(coreAccountInfo.getEmail());
        if (account == null) {
            return coreAccountInfo.getEmail();
        }
        boolean canHaveEmailAddressDisplayed =
                account.getAccountCapabilities().canHaveEmailAddressDisplayed() != Tribool.FALSE;
        return canHaveEmailAddressDisplayed ? account.getEmail() : account.getFullName();
    }

    private void startSyncFlow() {
        SyncService syncService = SyncServiceFactory.getForProfile(mProfile);
        if (syncService == null) return;
        if (!syncService.isSyncFeatureEnabled()) {
            mOptionsHandler.startSyncConsentFlow();
            return;
        }
        assert !syncService.getSelectedTypes().contains(UserSelectableType.PASSWORDS);
        mOptionsHandler.openSyncSettings();
    }

    @Override
    public void passwordListAvailable(int count) {
        mOptionsHandler.passwordsAvailable();
    }

    @Override
    public void passwordExceptionListAvailable(int count) {
        // This is unused.
    }

    private boolean shouldOfferSync() {
        if (!PasswordManagerBuildflags.USE_LOGIN_DATABASE_AS_BACKEND) {
            return false;
        }
        SigninManager signinManager = IdentityServicesProvider.get().getSigninManager(mProfile);
        if (signinManager == null || signinManager.isSigninDisabledByPolicy()) {
            return false;
        }
        SyncService syncService = SyncServiceFactory.getForProfile(mProfile);
        if (syncService == null) {
            return false;
        }
        if (syncService.isSyncDisabledByEnterprisePolicy()) {
            return false;
        }
        if (syncService.isTypeManagedByPolicy(UserSelectableType.PASSWORDS)) {
            return false;
        }
        return true;
    }

    private void resetTimestamp() {
        PrefService prefService = UserPrefs.get(mProfile);
        prefService.setString(Pref.LOCAL_PASSWORDS_MIGRATION_WARNING_SHOWN_TIMESTAMP, "0");
    }

    private void recordEmptySheetTrigger(@PasswordMigrationWarningTriggers int referrer) {
        RecordHistogram.recordEnumeratedHistogram(
                PasswordMetricsUtil.PASSWORD_MIGRATION_WARNING_EMPTY_SHEET_TRIGGER,
                referrer,
                PasswordMigrationWarningTriggers.MAX_VALUE);
    }

    private void recordSheetStateAtClosing(
            @BottomSheetController.StateChangeReason int reason, boolean setFragmentWasCalled) {
        RecordHistogram.recordEnumeratedHistogram(
                PasswordMetricsUtil.PASSWORD_MIGRATION_WARNING_SHEET_STATE_AT_CLOSING,
                getSheetStateAtClosingBucket(reason, setFragmentWasCalled),
                PasswordMigrationWarningSheetStateAtClosing.COUNT);
    }

    @PasswordMigrationWarningSheetStateAtClosing
    private int getSheetStateAtClosingBucket(
            @BottomSheetController.StateChangeReason int reason, boolean setFragmentWasCalled) {
        if (setFragmentWasCalled) {
            return PasswordMigrationWarningSheetStateAtClosing.FULL_SHEET_CLOSED;
        }
        switch (reason) {
            case StateChangeReason.SWIPE:
            case StateChangeReason.BACK_PRESS:
            case StateChangeReason.TAP_SCRIM:
            case StateChangeReason.NAVIGATION:
            case StateChangeReason.OMNIBOX_FOCUS:
                return PasswordMigrationWarningSheetStateAtClosing
                        .EMPTY_SHEET_CLOSED_BY_USER_INTERACTION;
            default:
                return PasswordMigrationWarningSheetStateAtClosing
                        .EMPTY_SHEET_CLOSED_WITHOUT_USER_INTERACTION;
        }
    }
}