chromium/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java

// Copyright 2021 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.ui.android.webid;

import android.content.res.Resources;

import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.blink.mojom.RpContext;
import org.chromium.blink.mojom.RpMode;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.chrome.browser.ui.android.webid.data.Account;
import org.chromium.chrome.browser.ui.android.webid.data.ClientIdMetadata;
import org.chromium.chrome.browser.ui.android.webid.data.IdentityCredentialTokenError;
import org.chromium.chrome.browser.ui.android.webid.data.IdentityProviderData;
import org.chromium.chrome.browser.ui.android.webid.data.IdentityProviderMetadata;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.content.webid.IdentityRequestDialogDismissReason;
import org.chromium.content.webid.IdentityRequestDialogLinkType;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;

import java.util.Arrays;

/**
 * This bridge creates and initializes a {@link AccountSelectionComponent} on construction and
 * forwards native calls to it.
 */
class AccountSelectionBridge implements AccountSelectionComponent.Delegate {
    /**
     * The size of the maskable icon's safe zone as a fraction of the icon's edge size as defined
     * in https://www.w3.org/TR/appmanifest/
     */
    public static final float MASKABLE_ICON_SAFE_ZONE_DIAMETER_RATIO = 0.8f;

    private long mNativeView;
    private final AccountSelectionComponent mAccountSelectionComponent;

    private AccountSelectionBridge(
            long nativeView,
            Tab tab,
            WindowAndroid windowAndroid,
            BottomSheetController bottomSheetController,
            @RpMode.EnumType int rpMode) {
        mNativeView = nativeView;
        mAccountSelectionComponent =
                new AccountSelectionCoordinator(
                        tab, windowAndroid, bottomSheetController, rpMode, this);
    }

    @CalledByNative
    static int getBrandIconMinimumSize(@RpMode.EnumType int rpMode) {
        // Icon needs to be big enough for the smallest screen density (1x).
        Resources resources = ContextUtils.getApplicationContext().getResources();
        // Density < 1.0f on ldpi devices. Adjust density to ensure that
        // {@link getBrandIconMinimumSize()} <= {@link getBrandIconIdealSize()}.
        float density = Math.max(resources.getDisplayMetrics().density, 1.0f);
        return Math.round(getBrandIconIdealSize(rpMode) / density);
    }

    @CalledByNative
    static int getBrandIconIdealSize(@RpMode.EnumType int rpMode) {
        Resources resources = ContextUtils.getApplicationContext().getResources();
        return Math.round(
                resources.getDimension(
                                rpMode == RpMode.BUTTON
                                        ? R.dimen.account_selection_button_mode_sheet_icon_size
                                        : R.dimen.account_selection_sheet_icon_size)
                        / MASKABLE_ICON_SAFE_ZONE_DIAMETER_RATIO);
    }

    @CalledByNative
    private static @Nullable AccountSelectionBridge create(
            long nativeView,
            WebContents webContents,
            WindowAndroid windowAndroid,
            @RpMode.EnumType int rpMode) {
        BottomSheetController bottomSheetController =
                BottomSheetControllerProvider.from(windowAndroid);
        if (bottomSheetController == null) return null;
        Tab tab = TabUtils.fromWebContents(webContents);
        return new AccountSelectionBridge(
                nativeView, tab, windowAndroid, bottomSheetController, rpMode);
    }

    @CalledByNative
    private void destroy() {
        mAccountSelectionComponent.close();
        mNativeView = 0;
    }

    /**
     * Shows the accounts in a bottom sheet UI allowing user to select one.
     *
     * @param rpForDisplay is the formatted RP URL to display in the FedCM prompt.
     * @param idpForDisplay is the formatted IDP URL to display in the FedCM prompt.
     * @param accounts is the list of accounts to be shown.
     * @param idpMetadata is the metadata of the IDP.
     * @param clientIdMetadata is the metadata of the RP.
     * @param isAutoReauthn represents whether this is an auto re-authn flow.
     * @param rpContext is an enum representing the desired text to be used in the title of the
     *     FedCM prompt: "signin", "continue", etc.
     * @param requestPermission A {@link boolean} indicating whether we need to request permission
     *     from the user to share their data with the IDP, if the user is not a returning user.
     */
    @CalledByNative
    private void showAccounts(
            @JniType("std::string") String rpForDisplay,
            @JniType("std::string") String idpForDisplay,
            Account[] accounts,
            IdentityProviderMetadata idpMetadata,
            ClientIdMetadata clientIdMetadata,
            boolean isAutoReauthn,
            @RpContext.EnumType int rpContext,
            boolean requestPermission,
            @Nullable IdentityProviderData newAccountsIdp) {
        assert accounts != null && accounts.length > 0;
        mAccountSelectionComponent.showAccounts(
                rpForDisplay,
                idpForDisplay,
                Arrays.asList(accounts),
                idpMetadata,
                clientIdMetadata,
                isAutoReauthn,
                rpContext,
                requestPermission,
                newAccountsIdp);
    }

    /**
     * Shows a bottomsheet prompting the user to sign in to an IDP for the purpose of federated
     * login when the IDP sign-in status is signin but no accounts are received from the fetch.
     *
     * @param rpForDisplay is the formatted RP URL to display in the FedCM prompt.
     * @param idpForDisplay is the formatted IDP URL to display in the FedCM prompt.
     * @param idpMetadata is the metadata of the IDP.
     * @param rpContext is an enum representing the desired text to be used in the title of the
     *     FedCM prompt: "signin", "continue", etc.
     */
    @CalledByNative
    private void showFailureDialog(
            @JniType("std::string") String rpForDisplay,
            @JniType("std::string") String idpForDisplay,
            IdentityProviderMetadata idpMetadata,
            @RpContext.EnumType int rpContext) {
        mAccountSelectionComponent.showFailureDialog(
                rpForDisplay, idpForDisplay, idpMetadata, rpContext);
    }

    /**
     * Shows a bottomsheet detailing the error that has occurred in the user's attempt to sign-in
     * through federated login.
     *
     * @param rpForDisplay is the formatted RP URL to display in the FedCM prompt.
     * @param idpForDisplay is the formatted IDP URL to display in the FedCM prompt.
     * @param idpMetadata is the metadata of the IDP.
     * @param rpContext is a {@link String} representing the desired text to be used in the title of
     *     the FedCM prompt: "signin", "continue", etc.
     * @param IdentityCredentialTokenError is contains the error code and url to display in the
     *     FedCM prompt.
     */
    @CalledByNative
    private void showErrorDialog(
            @JniType("std::string") String rpForDisplay,
            @JniType("std::string") String idpForDisplay,
            IdentityProviderMetadata idpMetadata,
            @RpContext.EnumType int rpContext,
            IdentityCredentialTokenError error) {
        mAccountSelectionComponent.showErrorDialog(
                rpForDisplay, idpForDisplay, idpMetadata, rpContext, error);
    }

    /**
     * Shows a bottomsheet prompting the user to sign-in to an RP with an IDP with a spinner to
     * indicate that contents are loading.
     *
     * @param rpForDisplay is the formatted RP URL to display in the FedCM prompt.
     * @param idpForDisplay is the formatted IDP URL to display in the FedCM prompt.
     * @param rpContext is a {@link String} representing the desired text to be used in the title of
     *     the FedCM prompt: "signin", "continue", etc.
     */
    @CalledByNative
    private void showLoadingDialog(
            @JniType("std::string") String rpForDisplay,
            @JniType("std::string") String idpForDisplay,
            @RpContext.EnumType int rpContext) {
        mAccountSelectionComponent.showLoadingDialog(rpForDisplay, idpForDisplay, rpContext);
    }

    @CalledByNative
    private @JniType("std::string") String getTitle() {
        return mAccountSelectionComponent.getTitle();
    }

    @CalledByNative
    private @JniType("std::optional<std::string>") String getSubtitle() {
        return mAccountSelectionComponent.getSubtitle();
    }

    @CalledByNative
    private void showUrl(@IdentityRequestDialogLinkType int linkType, @JniType("GURL") GURL url) {
        mAccountSelectionComponent.showUrl(linkType, url);
    }

    @CalledByNative
    private WebContents showModalDialog(@JniType("GURL") GURL url) {
        return mAccountSelectionComponent.showModalDialog(url);
    }

    @CalledByNative
    private void closeModalDialog() {
        mAccountSelectionComponent.closeModalDialog();
    }

    @CalledByNative
    private WebContents getRpWebContents() {
        return mAccountSelectionComponent.getRpWebContents();
    }

    @Override
    public void onDismissed(@IdentityRequestDialogDismissReason int dismissReason) {
        if (mNativeView != 0) {
            AccountSelectionBridgeJni.get().onDismiss(mNativeView, dismissReason);
        }
    }

    @Override
    public void onAccountSelected(GURL idpConfigUrl, Account account) {
        if (mNativeView != 0) {
            // This call passes the account fields directly as String and GURL parameters as an
            // optimization to avoid needing multiple JNI getters on the Account class on for each
            // field.
            AccountSelectionBridgeJni.get()
                    .onAccountSelected(
                            mNativeView,
                            idpConfigUrl,
                            account.getStringFields(),
                            account.getPictureUrl(),
                            account.isSignIn());
        }
    }

    @Override
    public void onLoginToIdP(GURL idpConfigUrl, GURL idpLoginUrl) {
        if (mNativeView != 0) {
            AccountSelectionBridgeJni.get().onLoginToIdP(mNativeView, idpConfigUrl, idpLoginUrl);
        }
    }

    @Override
    public void onMoreDetails() {
        if (mNativeView != 0) {
            AccountSelectionBridgeJni.get().onMoreDetails(mNativeView);
        }
    }

    @Override
    public void onAccountsDisplayed() {
        if (mNativeView != 0) {
            AccountSelectionBridgeJni.get().onAccountsDisplayed(mNativeView);
        }
    }

    @Override
    public void onModalDialogClosed() {
        mAccountSelectionComponent.onModalDialogClosed();
    }

    @Override
    public WebContents getWebContents() {
        return mAccountSelectionComponent.getWebContents();
    }

    @Override
    public void setPopupComponent(AccountSelectionComponent popupComponent) {
        mAccountSelectionComponent.setPopupComponent(popupComponent);
    }

    @NativeMethods
    interface Natives {
        void onAccountSelected(
                long nativeAccountSelectionViewAndroid,
                @JniType("GURL") GURL idpConfigUrl,
                @JniType("std::vector<std::string>") String[] accountFields,
                @JniType("GURL") GURL accountPictureUrl,
                boolean isSignedIn);

        void onDismiss(
                long nativeAccountSelectionViewAndroid,
                @IdentityRequestDialogDismissReason int dismissReason);

        void onLoginToIdP(
                long nativeAccountSelectionViewAndroid,
                @JniType("GURL") GURL idpConfigUrl,
                @JniType("GURL") GURL idpLoginUrl);

        void onMoreDetails(long nativeAccountSelectionViewAndroid);

        void onAccountsDisplayed(long nativeAccountSelectionViewAndroid);
    }
}