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