| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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.autofill_assistant; |
| |
| import android.accounts.Account; |
| import android.content.Context; |
| import android.graphics.RectF; |
| import android.os.Bundle; |
| import android.support.annotation.Nullable; |
| import android.telephony.TelephonyManager; |
| |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.LocaleUtils; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.JNINamespace; |
| import org.chromium.chrome.autofill_assistant.R; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager; |
| import org.chromium.components.signin.AccountManagerFacade; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.payments.mojom.PaymentOptions; |
| |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Bridge to native side autofill_assistant::UiControllerAndroid. It allows native side to control |
| * Autofill Assistant related UIs and forward UI events to native side. |
| */ |
| @JNINamespace("autofill_assistant") |
| public class AutofillAssistantUiController extends AbstractAutofillAssistantUiController { |
| /** OAuth2 scope that RPCs require. */ |
| private static final String AUTH_TOKEN_TYPE = |
| "oauth2:https://www.googleapis.com/auth/userinfo.profile"; |
| private static final String PARAMETER_USER_EMAIL = "USER_EMAIL"; |
| |
| private final WebContents mWebContents; |
| private final String mInitialUrl; |
| |
| // TODO(crbug.com/806868): Move mCurrentDetails and mStatusMessage to a Model (refactor to MVC). |
| private Details mCurrentDetails = Details.EMPTY_DETAILS; |
| private String mStatusMessage; |
| |
| /** Native pointer to the UIController. */ |
| private long mUiControllerAndroid; |
| |
| private UiDelegateHolder mUiDelegateHolder; |
| |
| /** |
| * Indicates whether {@link mAccount} has been initialized. |
| */ |
| private boolean mAccountInitialized; |
| |
| /** |
| * Account that was used to initiate AutofillAssistant. |
| * |
| * <p>This account is used to authenticate when sending RPCs and as default account for Payment |
| * Request. Not relevant until the accounts have been fetched, and mAccountInitialized set to |
| * true. Can still be null after the accounts are fetched, in which case authentication is |
| * disabled. |
| */ |
| @Nullable |
| private Account mAccount; |
| |
| /** If set, fetch the access token once the accounts are fetched. */ |
| private boolean mShouldFetchAccessToken; |
| |
| /** |
| * Construct Autofill Assistant UI controller. |
| * |
| * @param activity The ChromeActivity of the controller associated with. |
| */ |
| public AutofillAssistantUiController(ChromeActivity activity, Map<String, String> parameters) { |
| mWebContents = activity.getActivityTab().getWebContents(); |
| mInitialUrl = activity.getInitialIntent().getDataString(); |
| |
| mUiControllerAndroid = |
| nativeInit(mWebContents, parameters.keySet().toArray(new String[parameters.size()]), |
| parameters.values().toArray(new String[parameters.size()]), |
| LocaleUtils.getDefaultLocaleString(), getCountryIso()); |
| |
| chooseAccountAsync( |
| parameters.get(PARAMETER_USER_EMAIL), activity.getInitialIntent().getExtras()); |
| } |
| |
| @CalledByNative |
| private void onNativeDestroy() { |
| mUiControllerAndroid = 0; |
| } |
| |
| @Override |
| public void init(UiDelegateHolder delegateHolder, Details details) { |
| mUiDelegateHolder = delegateHolder; |
| maybeUpdateDetails(details); |
| if (mUiControllerAndroid != 0) nativeStart(mUiControllerAndroid, mInitialUrl); |
| } |
| |
| @Override |
| public void onDismiss() { |
| mUiDelegateHolder.dismiss(R.string.autofill_assistant_stopped); |
| } |
| |
| @Override |
| public Details getDetails() { |
| return mCurrentDetails; |
| } |
| |
| @Override |
| public String getStatusMessage() { |
| return mStatusMessage; |
| } |
| |
| @Override |
| public void onUnexpectedTaps() { |
| mUiDelegateHolder.dismiss(R.string.autofill_assistant_maybe_give_up); |
| } |
| |
| @Override |
| public void updateTouchableArea() { |
| if (mUiControllerAndroid != 0) nativeUpdateTouchableArea(mUiControllerAndroid); |
| } |
| |
| @Override |
| public void onUserInteractionInsideTouchableArea() { |
| if (mUiControllerAndroid != 0) |
| nativeOnUserInteractionInsideTouchableArea(mUiControllerAndroid); |
| } |
| |
| @Override |
| public void onScriptSelected(String scriptPath) { |
| if (mUiControllerAndroid != 0) nativeOnScriptSelected(mUiControllerAndroid, scriptPath); |
| } |
| |
| @Override |
| public void onChoice(byte[] serverPayload) { |
| if (mUiControllerAndroid != 0) nativeOnChoice(mUiControllerAndroid, serverPayload); |
| } |
| |
| @Override |
| public void onAddressSelected(String guid) { |
| if (mUiControllerAndroid != 0) nativeOnAddressSelected(mUiControllerAndroid, guid); |
| } |
| |
| @Override |
| public void onCardSelected(String guid) { |
| if (mUiControllerAndroid != 0) nativeOnCardSelected(mUiControllerAndroid, guid); |
| } |
| |
| @Override |
| public void onDetailsAcknowledged(Details displayedDetails, boolean canContinue) { |
| mCurrentDetails = displayedDetails; |
| if (mUiControllerAndroid != 0) nativeOnShowDetails(mUiControllerAndroid, canContinue); |
| } |
| |
| @Override |
| public String getDebugContext() { |
| if (mUiControllerAndroid == 0) return ""; |
| return nativeOnRequestDebugContext(mUiControllerAndroid); |
| } |
| |
| @Override |
| public void onCompleteShutdown() { |
| if (mUiControllerAndroid != 0) nativeDestroy(mUiControllerAndroid); |
| } |
| |
| @CalledByNative |
| private void onAllowShowingSoftKeyboard(boolean allowed) { |
| this.mUiDelegateHolder.performUiOperation( |
| uiDelegate -> uiDelegate.allowShowingSoftKeyboard(allowed)); |
| } |
| |
| @CalledByNative |
| private void onShowStatusMessage(String message) { |
| mStatusMessage = message; |
| mUiDelegateHolder.performUiOperation(uiDelegate -> uiDelegate.showStatusMessage(message)); |
| } |
| |
| @CalledByNative |
| private String onGetStatusMessage() { |
| return mStatusMessage; |
| } |
| |
| @CalledByNative |
| private void onShowOverlay() { |
| mUiDelegateHolder.performUiOperation(uiDelegate -> { |
| uiDelegate.showOverlay(); |
| uiDelegate.disableProgressBarPulsing(); |
| }); |
| } |
| |
| @CalledByNative |
| private void onHideOverlay() { |
| mUiDelegateHolder.performUiOperation(uiDelegate -> { |
| uiDelegate.hideOverlay(); |
| uiDelegate.enableProgressBarPulsing(); |
| }); |
| } |
| |
| @CalledByNative |
| private void onShutdown() { |
| mUiDelegateHolder.shutdown(); |
| } |
| |
| @CalledByNative |
| private void onClose() { |
| mUiDelegateHolder.close(); |
| } |
| |
| @CalledByNative |
| private void onShutdownGracefully() { |
| mUiDelegateHolder.enterGracefulShutdownMode(); |
| } |
| |
| @CalledByNative |
| private void onUpdateScripts( |
| String[] scriptNames, String[] scriptPaths, boolean[] scriptsHighlightFlags) { |
| assert scriptNames.length == scriptPaths.length; |
| assert scriptNames.length == scriptsHighlightFlags.length; |
| |
| List<AutofillAssistantUiDelegate.ScriptHandle> scriptHandles = new ArrayList<>(); |
| // Note that scriptNames, scriptsHighlightFlags and scriptPaths are one-on-one matched by |
| // index. |
| for (int i = 0; i < scriptNames.length; i++) { |
| scriptHandles.add(new AutofillAssistantUiDelegate.ScriptHandle( |
| scriptNames[i], scriptsHighlightFlags[i], scriptPaths[i])); |
| } |
| |
| mUiDelegateHolder.performUiOperation(uiDelegate -> uiDelegate.updateScripts(scriptHandles)); |
| } |
| |
| @CalledByNative |
| private void onChoose(String[] names, byte[][] serverPayloads, boolean[] highlightFlags) { |
| assert names.length == serverPayloads.length; |
| assert names.length == highlightFlags.length; |
| |
| // An empty choice list is supported, as selection can still be forced. onForceChoose should |
| // be a no-op in this case. |
| if (names.length == 0) return; |
| |
| List<AutofillAssistantUiDelegate.Choice> choices = new ArrayList<>(); |
| assert (names.length == serverPayloads.length); |
| for (int i = 0; i < names.length; i++) { |
| choices.add(new AutofillAssistantUiDelegate.Choice( |
| names[i], highlightFlags[i], serverPayloads[i])); |
| } |
| mUiDelegateHolder.performUiOperation(uiDelegate -> uiDelegate.showChoices(choices)); |
| } |
| |
| @CalledByNative |
| private void onForceChoose() { |
| mUiDelegateHolder.performUiOperation(uiDelegate -> uiDelegate.clearCarousel()); |
| } |
| |
| @CalledByNative |
| private void onChooseAddress() { |
| // TODO(crbug.com/806868): Remove this method once all scripts use payment request. |
| mUiDelegateHolder.performUiOperation(uiDelegate |
| -> uiDelegate.showProfiles(PersonalDataManager.getInstance().getProfilesToSuggest( |
| /* includeNameInLabel= */ true))); |
| } |
| |
| @CalledByNative |
| private void onChooseCard() { |
| // TODO(crbug.com/806868): Remove this method once all scripts use payment request. |
| mUiDelegateHolder.performUiOperation(uiDelegate |
| -> uiDelegate.showCards(PersonalDataManager.getInstance().getCreditCardsToSuggest( |
| /* includeServerCards= */ true))); |
| } |
| |
| @CalledByNative |
| private void onRequestPaymentInformation(boolean requestShipping, boolean requestPayerName, |
| boolean requestPayerPhone, boolean requestPayerEmail, int shippingType, String title, |
| String[] supportedBasicCardNetworks) { |
| PaymentOptions paymentOptions = new PaymentOptions(); |
| paymentOptions.requestShipping = requestShipping; |
| paymentOptions.requestPayerName = requestPayerName; |
| paymentOptions.requestPayerPhone = requestPayerPhone; |
| paymentOptions.requestPayerEmail = requestPayerEmail; |
| paymentOptions.shippingType = shippingType; |
| |
| String defaultEmail = mAccount != null ? mAccount.name : ""; |
| |
| mUiDelegateHolder.performUiOperation(uiDelegate -> { |
| uiDelegate.showPaymentRequest(mWebContents, paymentOptions, title, |
| supportedBasicCardNetworks, defaultEmail, (selectedPaymentInformation -> { |
| uiDelegate.closePaymentRequest(); |
| if (selectedPaymentInformation.succeed) { |
| if (mUiControllerAndroid != 0) { |
| nativeOnGetPaymentInformation(mUiControllerAndroid, |
| selectedPaymentInformation.succeed, |
| selectedPaymentInformation.card, |
| selectedPaymentInformation.address, |
| selectedPaymentInformation.payerName, |
| selectedPaymentInformation.payerPhone, |
| selectedPaymentInformation.payerEmail, |
| selectedPaymentInformation.isTermsAndConditionsAccepted); |
| } |
| } else { |
| // A failed payment request flow indicates that the UI was either |
| // dismissed or the back button was clicked. In that case we gracefully |
| // shut down. |
| mUiDelegateHolder.giveUp(); |
| } |
| })); |
| }); |
| } |
| |
| /** |
| * Updates the currently shown details. |
| * |
| * @param newDetails details to display. |
| */ |
| void maybeUpdateDetails(Details newDetails) { |
| if (mCurrentDetails.isEmpty() && newDetails.isEmpty()) { |
| // No update on UI needed. |
| if (mUiControllerAndroid != 0) { |
| nativeOnShowDetails(mUiControllerAndroid, /* canContinue= */ true); |
| } |
| return; |
| } |
| |
| Details mergedDetails = Details.merge(mCurrentDetails, newDetails); |
| mUiDelegateHolder.performUiOperation(uiDelegate -> uiDelegate.showDetails(mergedDetails)); |
| } |
| |
| @CalledByNative |
| private void onHideDetails() { |
| mUiDelegateHolder.performUiOperation(AutofillAssistantUiDelegate::hideDetails); |
| } |
| |
| @CalledByNative |
| private void onShowDetails(String title, String url, String description, String mId, int year, |
| int month, int day, int hour, int minute, int second) { |
| Date date; |
| if (year > 0 && month > 0 && day > 0 && hour >= 0 && minute >= 0 && second >= 0) { |
| Calendar calendar = Calendar.getInstance(); |
| calendar.clear(); |
| // Month in Java Date is 0-based, but the one we receive from the server is 1-based. |
| calendar.set(year, month - 1, day, hour, minute, second); |
| date = calendar.getTime(); |
| } else { |
| date = null; |
| } |
| |
| maybeUpdateDetails(new Details( |
| title, url, date, description, mId, /* isFinal= */ true, Collections.emptySet())); |
| } |
| |
| @CalledByNative |
| private void onShowProgressBar(int progress, String message) { |
| mUiDelegateHolder.performUiOperation( |
| uiDelegate -> uiDelegate.showProgressBar(progress, message)); |
| } |
| |
| @CalledByNative |
| private void onHideProgressBar() { |
| mUiDelegateHolder.performUiOperation(AutofillAssistantUiDelegate::hideProgressBar); |
| } |
| |
| @CalledByNative |
| private void updateTouchableArea(boolean enabled, float[] coords) { |
| List<RectF> boxes = new ArrayList<>(); |
| for (int i = 0; i < coords.length; i += 4) { |
| boxes.add(new RectF(/* left= */ coords[i], /* top= */ coords[i + 1], |
| /* right= */ coords[i + 2], /* bottom= */ coords[i + 3])); |
| } |
| mUiDelegateHolder.performUiOperation( |
| uiDelegate -> { uiDelegate.updateTouchableArea(enabled, boxes); }); |
| } |
| |
| |
| @CalledByNative |
| private void fetchAccessToken() { |
| if (!mAccountInitialized) { |
| // Still getting the account list. Fetch the token as soon as an account is available. |
| mShouldFetchAccessToken = true; |
| return; |
| } |
| if (mAccount == null) { |
| if (mUiControllerAndroid != 0) nativeOnAccessToken(mUiControllerAndroid, true, ""); |
| return; |
| } |
| |
| AccountManagerFacade.get().getAuthToken( |
| mAccount, AUTH_TOKEN_TYPE, new AccountManagerFacade.GetAuthTokenCallback() { |
| @Override |
| public void tokenAvailable(String token) { |
| if (mUiControllerAndroid != 0) { |
| nativeOnAccessToken(mUiControllerAndroid, true, token); |
| } |
| } |
| |
| @Override |
| public void tokenUnavailable(boolean isTransientError) { |
| if (!isTransientError && mUiControllerAndroid != 0) { |
| nativeOnAccessToken(mUiControllerAndroid, false, ""); |
| } |
| } |
| }); |
| } |
| |
| @CalledByNative |
| private void invalidateAccessToken(String accessToken) { |
| if (mAccount == null) { |
| return; |
| } |
| |
| AccountManagerFacade.get().invalidateAuthToken(accessToken); |
| } |
| |
| /** Choose an account to authenticate as for making RPCs to the backend. */ |
| private void chooseAccountAsync(@Nullable String accountFromParameter, Bundle extras) { |
| AccountManagerFacade.get().tryGetGoogleAccounts(accounts -> { |
| if (mUiControllerAndroid == 0) return; |
| if (accounts.size() == 1) { |
| // If there's only one account, there aren't any doubts. |
| onAccountChosen(accounts.get(0)); |
| return; |
| } |
| Account signedIn = |
| findAccountByName(accounts, nativeGetPrimaryAccountName(mUiControllerAndroid)); |
| if (signedIn != null) { |
| // TODO(crbug.com/806868): Compare against account name from extras and complain if |
| // they don't match. |
| onAccountChosen(signedIn); |
| return; |
| } |
| |
| if (accountFromParameter != null) { |
| Account account = findAccountByName(accounts, accountFromParameter); |
| if (account != null) { |
| onAccountChosen(account); |
| return; |
| } |
| } |
| |
| for (String extra : extras.keySet()) { |
| // TODO(crbug.com/806868): Deprecate ACCOUNT_NAME. |
| if (extra.endsWith("ACCOUNT_NAME")) { |
| Account account = findAccountByName(accounts, extras.getString(extra)); |
| if (account != null) { |
| onAccountChosen(account); |
| return; |
| } |
| } |
| } |
| onAccountChosen(null); |
| }); |
| } |
| |
| private void onAccountChosen(@Nullable Account account) { |
| mAccount = account; |
| mAccountInitialized = true; |
| // TODO(crbug.com/806868): Consider providing a way of signing in this case, to enforce |
| // that all calls are authenticated. |
| |
| if (mShouldFetchAccessToken) { |
| mShouldFetchAccessToken = false; |
| fetchAccessToken(); |
| } |
| } |
| |
| private static Account findAccountByName(List<Account> accounts, String name) { |
| for (int i = 0; i < accounts.size(); i++) { |
| Account account = accounts.get(i); |
| if (account.name.equals(name)) { |
| return account; |
| } |
| } |
| return null; |
| } |
| |
| /** Returns the country that the device is currently located in. This currently only works |
| * for devices with active SIM cards. For a more general solution, we should probably use |
| * the LocationManager together with the Geocoder.*/ |
| private String getCountryIso() { |
| TelephonyManager telephonyManager = |
| (TelephonyManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.TELEPHONY_SERVICE); |
| |
| // According to API, location for CDMA networks is unreliable |
| if (telephonyManager != null |
| && telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) |
| return telephonyManager.getNetworkCountryIso(); |
| else |
| return null; |
| } |
| |
| // native methods. |
| private native long nativeInit(WebContents webContents, String[] parameterNames, |
| String[] parameterValues, String locale, String countryCode); |
| private native void nativeStart(long nativeUiControllerAndroid, String initialUrl); |
| private native void nativeDestroy(long nativeUiControllerAndroid); |
| private native void nativeUpdateTouchableArea(long nativeUiControllerAndroid); |
| private native void nativeOnUserInteractionInsideTouchableArea(long nativeUiControllerAndroid); |
| private native void nativeOnScriptSelected(long nativeUiControllerAndroid, String scriptPath); |
| private native void nativeOnChoice(long nativeUiControllerAndroid, byte[] serverPayload); |
| private native void nativeOnAddressSelected(long nativeUiControllerAndroid, String guid); |
| private native void nativeOnCardSelected(long nativeUiControllerAndroid, String guid); |
| private native void nativeOnShowDetails(long nativeUiControllerAndroid, boolean canContinue); |
| private native void nativeOnGetPaymentInformation(long nativeUiControllerAndroid, |
| boolean succeed, @Nullable PersonalDataManager.CreditCard card, |
| @Nullable PersonalDataManager.AutofillProfile address, @Nullable String payerName, |
| @Nullable String payerPhone, @Nullable String payerEmail, |
| boolean isTermsAndConditionsAccepted); |
| private native void nativeOnAccessToken( |
| long nativeUiControllerAndroid, boolean success, String accessToken); |
| private native String nativeGetPrimaryAccountName(long nativeUiControllerAndroid); |
| private native String nativeOnRequestDebugContext(long nativeUiControllerAndroid); |
| } |