chromium/chrome/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerErrorMessageHelperBridge.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.password_manager;

import android.app.Activity;

import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;

import org.chromium.base.IntentUtils;
import org.chromium.base.TimeUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.sync.TrustedVaultClient;
import org.chromium.chrome.browser.sync.ui.SyncTrustedVaultProxyActivity;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.signin.AccountManagerFacadeProvider;
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.TrustedVaultUserActionTriggerForUMA;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.ui.base.WindowAndroid;

import java.util.concurrent.TimeUnit;

/** The bridge provides a way to interact with the Android sign in flow. */
public class PasswordManagerErrorMessageHelperBridge {
    @VisibleForTesting
    static final long MINIMAL_INTERVAL_BETWEEN_PROMPTS_MS =
            TimeUnit.MILLISECONDS.convert(24, TimeUnit.HOURS);

    @VisibleForTesting
    static final long MINIMAL_INTERVAL_TO_SYNC_ERROR_MS =
            TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES);

    /**
     * Checks whether the right amount of time has passed since the last error UI messages were
     * shown.
     *
     * <p>The error UI should be shown at least {@link #MINIMAL_INTERVAL_BETWEEN_PROMPTS_MS} from
     * the previous one and at least {@link #MINIMAL_INTERVAL_TO_SYNC_ERROR_MS} from the last sync
     * error UI.
     *
     * @return whether the UI can be shown given the conditions above.
     */
    @CalledByNative
    static boolean shouldShowSignInErrorUI(Profile profile) {
        final IdentityManager identityManager =
                IdentityServicesProvider.get().getIdentityManager(profile);
        if (identityManager == null) return false;

        // It is possible that the account is removed from Chrome between the password manager
        // calling the Google Play Services backend and Chrome receiving the reply. In that
        // case, the error is no longer relevant/fixable.
        if (identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN) == null) return false;

        PrefService prefService = UserPrefs.get(profile);
        long lastShownTimestamp =
                Long.valueOf(prefService.getString(Pref.UPM_ERROR_UI_SHOWN_TIMESTAMP));
        long lastShownSyncErrorTimestamp =
                ChromeSharedPreferences.getInstance()
                        .readLong(ChromePreferenceKeys.SYNC_ERROR_MESSAGE_SHOWN_AT_TIME, 0);
        long currentTime = TimeUtils.currentTimeMillis();
        return (currentTime - lastShownTimestamp > MINIMAL_INTERVAL_BETWEEN_PROMPTS_MS)
                && (currentTime - lastShownSyncErrorTimestamp) > MINIMAL_INTERVAL_TO_SYNC_ERROR_MS;
    }

    /**
     * Checks whether the right amount of time has passed since the last error UI messages were
     * shown.
     *
     * <p>The error UI should be shown at least {@link #MINIMAL_INTERVAL_BETWEEN_PROMPTS_MS} from
     * the previous one.
     *
     * @return whether the UI can be shown given the conditions above.
     */
    @CalledByNative
    static boolean shouldShowUpdateGMSCoreErrorUI(Profile profile) {
        PrefService prefService = UserPrefs.get(profile);
        long lastShownTimestamp =
                Long.valueOf(prefService.getString(Pref.UPM_ERROR_UI_SHOWN_TIMESTAMP));
        long currentTime = TimeUtils.currentTimeMillis();
        return currentTime - lastShownTimestamp > MINIMAL_INTERVAL_BETWEEN_PROMPTS_MS;
    }

    /** Saves the timestamp in ms since UNIX epoch at which the error UI was shown. */
    @CalledByNative
    static void saveErrorUiShownTimestamp(Profile profile) {
        PrefService prefService = UserPrefs.get(profile);
        prefService.setString(
                Pref.UPM_ERROR_UI_SHOWN_TIMESTAMP, Long.toString(TimeUtils.currentTimeMillis()));
    }

    /**
     * Starts the Android process to update credentials for the primary account in Chrome. This
     * method will only work for users that have been previously signed in Chrome on the device.
     */
    @CalledByNative
    static void startUpdateAccountCredentialsFlow(WindowAndroid windowAndroid, Profile profile) {
        final CoreAccountInfo primaryAccountInfo =
                IdentityServicesProvider.get()
                        .getIdentityManager(profile)
                        .getPrimaryAccountInfo(ConsentLevel.SIGNIN);
        // If the account has been removed before calling this method, there are no credentials to
        // update.
        if (primaryAccountInfo == null) return;
        final Activity activity = windowAndroid.getActivity().get();
        AccountManagerFacadeProvider.getInstance()
                .updateCredentials(
                        CoreAccountInfo.getAndroidAccountFrom(primaryAccountInfo),
                        activity,
                        (success) -> {
                            RecordHistogram.recordBooleanHistogram(
                                    "PasswordManager.UPMUpdateSignInCredentialsSucces", success);
                        });
    }

    /**
     * Starts the Android process to retrieve encryption keys in Chrome. This method will only work for users that have been previously syncing in Chrome.
     */
    @CalledByNative
    static void startTrustedVaultKeyRetrievalFlow(WindowAndroid windowAndroid, Profile profile) {
        final CoreAccountInfo primaryAccountInfo =
                SyncServiceFactory.getForProfile(profile).getAccountInfo();
        // If the account has been removed before calling this method, there is nothing to do.
        if (primaryAccountInfo == null) return;
        final Activity activity = windowAndroid.getActivity().get();

        TrustedVaultClient.get()
                .createKeyRetrievalIntent(primaryAccountInfo)
                .then(
                        (intent) -> {
                            var action =
                                    TrustedVaultUserActionTriggerForUMA
                                            .PASSWORD_MANAGER_ERROR_MESSAGE;
                            var proxyIntent =
                                    SyncTrustedVaultProxyActivity.createKeyRetrievalProxyIntent(
                                            intent, action);
                            IntentUtils.safeStartActivity(activity, proxyIntent);
                        });
    }

    /** Starts the Google Play services page where the user can choose to update GMSCore. */
    @CalledByNative
    static void launchGmsUpdate(WindowAndroid windowAndroid) {
        assert windowAndroid.getActivity().get() != null;
        Activity activity = windowAndroid.getActivity().get();
        PasswordManagerHelper.launchGmsUpdate(activity);
    }
}