1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.autofill; 6 7 import android.content.Context; 8 import android.text.Editable; 9 import android.text.TextWatcher; 10 import android.view.LayoutInflater; 11 import android.view.View; 12 import android.view.inputmethod.EditorInfo; 13 import android.widget.EditText; 14 import android.widget.ImageView; 15 import android.widget.PopupWindow; 16 import android.widget.TextView; 17 import android.widget.TextView.BufferType; 18 19 import androidx.core.text.TextUtilsCompat; 20 import androidx.core.view.ViewCompat; 21 22 import org.chromium.chrome.R; 23 import org.chromium.chrome.browser.app.ChromeActivity; 24 import org.chromium.ui.modaldialog.DialogDismissalCause; 25 import org.chromium.ui.modaldialog.ModalDialogManager; 26 import org.chromium.ui.modaldialog.ModalDialogProperties; 27 import org.chromium.ui.modelutil.PropertyModel; 28 29 import java.util.Locale; 30 /** 31 * Prompt that asks users to confirm user's name before saving card to Google. 32 */ 33 public class AutofillNameFixFlowPrompt implements TextWatcher, ModalDialogProperties.Controller { 34 /** 35 * An interface to handle the interaction with 36 * an AutofillNameFixFlowPrompt object. 37 */ 38 public interface AutofillNameFixFlowPromptDelegate { 39 /** 40 * Called when dialog is dismissed. 41 */ onPromptDismissed()42 void onPromptDismissed(); 43 44 /** 45 * Called when user accepted/confirmed the prompt. 46 * 47 * @param name Card holder name. 48 */ onUserAccept(String name)49 void onUserAccept(String name); 50 } 51 52 private final AutofillNameFixFlowPromptDelegate mDelegate; 53 private final PropertyModel mDialogModel; 54 55 private final View mDialogView; 56 private final EditText mUserNameInput; 57 private final ImageView mNameFixFlowTooltipIcon; 58 private PopupWindow mNameFixFlowTooltipPopup; 59 60 private ModalDialogManager mModalDialogManager; 61 private Context mContext; 62 63 /** 64 * Fix flow prompt to confirm user name before saving the card to Google. 65 */ AutofillNameFixFlowPrompt(Context context, AutofillNameFixFlowPromptDelegate delegate, String title, String inferredName, String confirmButtonLabel, int drawableId)66 public AutofillNameFixFlowPrompt(Context context, AutofillNameFixFlowPromptDelegate delegate, 67 String title, String inferredName, String confirmButtonLabel, int drawableId) { 68 mDelegate = delegate; 69 LayoutInflater inflater = LayoutInflater.from(context); 70 mDialogView = inflater.inflate(R.layout.autofill_name_fixflow, null); 71 72 mUserNameInput = (EditText) mDialogView.findViewById(R.id.cc_name_edit); 73 mUserNameInput.setText(inferredName, BufferType.EDITABLE); 74 mNameFixFlowTooltipIcon = (ImageView) mDialogView.findViewById(R.id.cc_name_tooltip_icon); 75 mNameFixFlowTooltipIcon.setOnClickListener((view) -> onTooltipIconClicked()); 76 77 PropertyModel.Builder builder = 78 new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS) 79 .with(ModalDialogProperties.CONTROLLER, this) 80 .with(ModalDialogProperties.TITLE, title) 81 .with(ModalDialogProperties.CUSTOM_VIEW, mDialogView) 82 .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, confirmButtonLabel) 83 .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, context.getResources(), 84 R.string.cancel) 85 .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, false) 86 .with(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, 87 inferredName.isEmpty()); 88 if (drawableId != 0) { 89 builder.with(ModalDialogProperties.TITLE_ICON, context, drawableId); 90 } 91 mDialogModel = builder.build(); 92 93 // Hitting the "submit" button on the software keyboard should submit, unless the name field 94 // is empty. 95 mUserNameInput.setOnEditorActionListener((view, actionId, event) -> { 96 if (actionId == EditorInfo.IME_ACTION_DONE) { 97 if (mUserNameInput.getText().toString().trim().length() != 0) { 98 onClick(mDialogModel, ModalDialogProperties.ButtonType.POSITIVE); 99 } 100 return true; 101 } 102 return false; 103 }); 104 } 105 106 /** 107 * Show the dialog. If activity is null this method will not do anything. 108 */ show(ChromeActivity activity)109 public void show(ChromeActivity activity) { 110 if (activity == null) return; 111 112 mContext = activity; 113 mModalDialogManager = activity.getModalDialogManager(); 114 mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.APP); 115 mUserNameInput.addTextChangedListener(this); 116 } 117 dismiss(@ialogDismissalCause int dismissalCause)118 protected void dismiss(@DialogDismissalCause int dismissalCause) { 119 mModalDialogManager.dismissDialog(mDialogModel, dismissalCause); 120 } 121 122 @Override afterTextChanged(Editable s)123 public void afterTextChanged(Editable s) { 124 mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, 125 mUserNameInput.getText().toString().trim().isEmpty()); 126 } 127 128 @Override beforeTextChanged(CharSequence s, int start, int count, int after)129 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 130 131 @Override onTextChanged(CharSequence s, int start, int before, int count)132 public void onTextChanged(CharSequence s, int start, int before, int count) {} 133 134 /** 135 * Handle tooltip icon clicked. If tooltip is already opened, don't show another. Otherwise 136 * create a new one. 137 */ onTooltipIconClicked()138 private void onTooltipIconClicked() { 139 if (mNameFixFlowTooltipPopup != null) return; 140 141 mNameFixFlowTooltipPopup = new PopupWindow(mContext); 142 Runnable dismissAction = () -> { 143 mNameFixFlowTooltipPopup = null; 144 }; 145 boolean isLeftToRight = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) 146 == ViewCompat.LAYOUT_DIRECTION_LTR; 147 AutofillUiUtils.showTooltip(mContext, mNameFixFlowTooltipPopup, 148 R.string.autofill_save_card_prompt_cardholder_name_tooltip, 149 new AutofillUiUtils.OffsetProvider() { 150 @Override 151 public int getXOffset(TextView textView) { 152 int xOffset = 153 mNameFixFlowTooltipIcon.getLeft() - textView.getMeasuredWidth(); 154 return Math.max(0, xOffset); 155 } 156 157 @Override 158 public int getYOffset(TextView textView) { 159 return 0; 160 } 161 }, 162 // If the layout is right to left then anchor on the edit text field else anchor on 163 // the tooltip icon, which would be on the left. 164 isLeftToRight ? mUserNameInput : mNameFixFlowTooltipIcon, dismissAction); 165 } 166 167 @Override onClick(PropertyModel model, int buttonType)168 public void onClick(PropertyModel model, int buttonType) { 169 if (buttonType == ModalDialogProperties.ButtonType.POSITIVE) { 170 mDelegate.onUserAccept(mUserNameInput.getText().toString()); 171 mModalDialogManager.dismissDialog(model, DialogDismissalCause.POSITIVE_BUTTON_CLICKED); 172 } else if (buttonType == ModalDialogProperties.ButtonType.NEGATIVE) { 173 mModalDialogManager.dismissDialog(model, DialogDismissalCause.NEGATIVE_BUTTON_CLICKED); 174 } 175 } 176 177 @Override onDismiss(PropertyModel model, int dismissalCause)178 public void onDismiss(PropertyModel model, int dismissalCause) { 179 // Do not call dismissed on the delegate if dialog was dismissed either because the user 180 // accepted to save the card or was dismissed by native code. 181 if (dismissalCause != DialogDismissalCause.POSITIVE_BUTTON_CLICKED 182 && dismissalCause != DialogDismissalCause.DISMISSED_BY_NATIVE) { 183 mDelegate.onPromptDismissed(); 184 } 185 } 186 } 187