1 // Copyright (c) 2012 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 #include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
6
7 #include <algorithm>
8 #include <utility>
9
10 #include "base/bind.h"
11 #include "base/check_op.h"
12 #include "base/command_line.h"
13 #include "base/i18n/rtl.h"
14 #include "base/macros.h"
15 #include "base/optional.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "build/build_config.h"
18 #include "chrome/browser/accessibility/accessibility_state_utils.h"
19 #include "chrome/browser/ui/autofill/autofill_popup_view.h"
20 #include "components/autofill/content/browser/content_autofill_driver.h"
21 #include "components/autofill/core/browser/ui/autofill_popup_delegate.h"
22 #include "components/autofill/core/browser/ui/popup_item_ids.h"
23 #include "components/autofill/core/browser/ui/suggestion.h"
24 #include "content/public/browser/native_web_keyboard_event.h"
25 #include "content/public/browser/render_widget_host_view.h"
26 #include "content/public/browser/web_contents.h"
27 #include "ui/accessibility/ax_active_popup.h"
28 #include "ui/accessibility/ax_tree_id.h"
29 #include "ui/accessibility/ax_tree_manager_map.h"
30 #include "ui/accessibility/platform/ax_platform_node.h"
31 #include "ui/events/event.h"
32 #include "ui/gfx/canvas.h"
33 #include "ui/gfx/text_elider.h"
34 #include "ui/gfx/text_utils.h"
35 #include "ui/views/accessibility/view_accessibility.h"
36
37 #if defined(OS_ANDROID)
38 #include "chrome/browser/autofill/manual_filling_controller_impl.h"
39
40 using FillingSource = ManualFillingController::FillingSource;
41 #endif
42
43 using base::WeakPtr;
44
45 namespace autofill {
46
47 #if !defined(OS_MAC)
48 // static
GetOrCreate(WeakPtr<AutofillPopupControllerImpl> previous,WeakPtr<AutofillPopupDelegate> delegate,content::WebContents * web_contents,gfx::NativeView container_view,const gfx::RectF & element_bounds,base::i18n::TextDirection text_direction)49 WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetOrCreate(
50 WeakPtr<AutofillPopupControllerImpl> previous,
51 WeakPtr<AutofillPopupDelegate> delegate,
52 content::WebContents* web_contents,
53 gfx::NativeView container_view,
54 const gfx::RectF& element_bounds,
55 base::i18n::TextDirection text_direction) {
56 if (previous.get() && previous->delegate_.get() == delegate.get() &&
57 previous->container_view() == container_view) {
58 previous->SetElementBounds(element_bounds);
59 previous->ClearState();
60 return previous;
61 }
62
63 if (previous.get())
64 previous->Hide(PopupHidingReason::kViewDestroyed);
65
66 AutofillPopupControllerImpl* controller = new AutofillPopupControllerImpl(
67 delegate, web_contents, container_view, element_bounds, text_direction);
68 return controller->GetWeakPtr();
69 }
70 #endif
71
AutofillPopupControllerImpl(base::WeakPtr<AutofillPopupDelegate> delegate,content::WebContents * web_contents,gfx::NativeView container_view,const gfx::RectF & element_bounds,base::i18n::TextDirection text_direction)72 AutofillPopupControllerImpl::AutofillPopupControllerImpl(
73 base::WeakPtr<AutofillPopupDelegate> delegate,
74 content::WebContents* web_contents,
75 gfx::NativeView container_view,
76 const gfx::RectF& element_bounds,
77 base::i18n::TextDirection text_direction)
78 : controller_common_(element_bounds, text_direction, container_view),
79 web_contents_(web_contents),
80 delegate_(delegate) {
81 ClearState();
82 delegate->RegisterDeletionCallback(base::BindOnce(
83 &AutofillPopupControllerImpl::HideViewAndDie, GetWeakPtr()));
84 }
85
86 AutofillPopupControllerImpl::~AutofillPopupControllerImpl() = default;
87
Show(const std::vector<Suggestion> & suggestions,bool autoselect_first_suggestion,PopupType popup_type)88 void AutofillPopupControllerImpl::Show(
89 const std::vector<Suggestion>& suggestions,
90 bool autoselect_first_suggestion,
91 PopupType popup_type) {
92 SetValues(suggestions);
93
94 bool just_created = false;
95 if (!view_) {
96 view_ = AutofillPopupView::Create(GetWeakPtr());
97
98 // It is possible to fail to create the popup, in this case
99 // treat the popup as hiding right away.
100 if (!view_) {
101 delegate_->OnPopupSuppressed();
102 Hide(PopupHidingReason::kViewDestroyed);
103 return;
104 }
105 just_created = true;
106 }
107
108 if (just_created) {
109 #if defined(OS_ANDROID)
110 ManualFillingController::GetOrCreate(web_contents_)
111 ->UpdateSourceAvailability(FillingSource::AUTOFILL,
112 !suggestions.empty());
113 #endif
114 WeakPtr<AutofillPopupControllerImpl> weak_this = GetWeakPtr();
115 view_->Show();
116 // crbug.com/1055981. |this| can be destroyed synchronously at this point.
117 if (!weak_this)
118 return;
119
120 // We only fire the event when a new popup shows. We do not fire the
121 // event when suggestions changed.
122 FireControlsChangedEvent(true);
123
124 if (autoselect_first_suggestion)
125 SetSelectedLine(0);
126 } else {
127 if (selected_line_ && *selected_line_ >= GetLineCount())
128 selected_line_.reset();
129
130 OnSuggestionsChanged();
131 }
132
133 static_cast<ContentAutofillDriver*>(delegate_->GetAutofillDriver())
134 ->RegisterKeyPressHandler(base::BindRepeating(
135 [](base::WeakPtr<AutofillPopupControllerImpl> weak_this,
136 const content::NativeWebKeyboardEvent& event) {
137 return weak_this && weak_this->HandleKeyPressEvent(event);
138 },
139 GetWeakPtr()));
140
141 delegate_->OnPopupShown();
142 }
143
UpdateDataListValues(const std::vector<base::string16> & values,const std::vector<base::string16> & labels)144 void AutofillPopupControllerImpl::UpdateDataListValues(
145 const std::vector<base::string16>& values,
146 const std::vector<base::string16>& labels) {
147 selected_line_.reset();
148 // Remove all the old data list values, which should always be at the top of
149 // the list if they are present.
150 while (!suggestions_.empty() &&
151 suggestions_[0].frontend_id == POPUP_ITEM_ID_DATALIST_ENTRY) {
152 suggestions_.erase(suggestions_.begin());
153 }
154
155 // If there are no new data list values, exit (clearing the separator if there
156 // is one).
157 if (values.empty()) {
158 if (!suggestions_.empty() &&
159 suggestions_[0].frontend_id == POPUP_ITEM_ID_SEPARATOR) {
160 suggestions_.erase(suggestions_.begin());
161 }
162
163 // The popup contents have changed, so either update the bounds or hide it.
164 if (HasSuggestions())
165 OnSuggestionsChanged();
166 else
167 Hide(PopupHidingReason::kNoSuggestions);
168
169 return;
170 }
171
172 // Add a separator if there are any other values.
173 if (!suggestions_.empty() &&
174 suggestions_[0].frontend_id != POPUP_ITEM_ID_SEPARATOR) {
175 suggestions_.insert(suggestions_.begin(), Suggestion());
176 suggestions_[0].frontend_id = POPUP_ITEM_ID_SEPARATOR;
177 }
178
179 // Prepend the parameters to the suggestions we already have.
180 suggestions_.insert(suggestions_.begin(), values.size(), Suggestion());
181 for (size_t i = 0; i < values.size(); i++) {
182 suggestions_[i].value = values[i];
183 suggestions_[i].label = labels[i];
184 suggestions_[i].frontend_id = POPUP_ITEM_ID_DATALIST_ENTRY;
185 }
186
187 OnSuggestionsChanged();
188 }
189
PinView()190 void AutofillPopupControllerImpl::PinView() {
191 is_view_pinned_ = true;
192 }
193
194 base::span<const Suggestion>
GetUnelidedSuggestions() const195 AutofillPopupControllerImpl::GetUnelidedSuggestions() const {
196 return base::span<const Suggestion>(suggestions_);
197 }
198
Hide(PopupHidingReason reason)199 void AutofillPopupControllerImpl::Hide(PopupHidingReason reason) {
200 // If the reason for hiding is only stale data or a user interacting with
201 // native Chrome UI (kFocusChanged/kEndEditing), the popup might be kept open.
202 if (is_view_pinned_ && (reason == PopupHidingReason::kStaleData ||
203 reason == PopupHidingReason::kFocusChanged ||
204 reason == PopupHidingReason::kEndEditing)) {
205 return; // Don't close the popup while waiting for an update.
206 }
207 // For tests, keep open when hiding is due to external stimuli.
208 if (keep_popup_open_for_testing_ &&
209 reason == PopupHidingReason::kWidgetChanged)
210 return; // Don't close the popup because the browser window is resized.
211 if (delegate_) {
212 delegate_->ClearPreviewedForm();
213 delegate_->OnPopupHidden();
214 static_cast<ContentAutofillDriver*>(delegate_->GetAutofillDriver())
215 ->RemoveKeyPressHandler();
216 }
217
218 HideViewAndDie();
219 }
220
ViewDestroyed()221 void AutofillPopupControllerImpl::ViewDestroyed() {
222 // The view has already been destroyed so clear the reference to it.
223 view_ = nullptr;
224
225 Hide(PopupHidingReason::kViewDestroyed);
226 }
227
HandleKeyPressEvent(const content::NativeWebKeyboardEvent & event)228 bool AutofillPopupControllerImpl::HandleKeyPressEvent(
229 const content::NativeWebKeyboardEvent& event) {
230 switch (event.windows_key_code) {
231 case ui::VKEY_UP:
232 SelectPreviousLine();
233 return true;
234 case ui::VKEY_DOWN:
235 SelectNextLine();
236 return true;
237 case ui::VKEY_PRIOR: // Page up.
238 // Set no line and then select the next line in case the first line is not
239 // selectable.
240 SetSelectedLine(base::nullopt);
241 SelectNextLine();
242 return true;
243 case ui::VKEY_NEXT: // Page down.
244 SetSelectedLine(GetLineCount() - 1);
245 return true;
246 case ui::VKEY_ESCAPE:
247 Hide(PopupHidingReason::kUserAborted);
248 return true;
249 case ui::VKEY_DELETE:
250 return (event.GetModifiers() &
251 content::NativeWebKeyboardEvent::kShiftKey) &&
252 RemoveSelectedLine();
253 case ui::VKEY_TAB:
254 // A tab press should cause the selected line to be accepted, but still
255 // return false so the tab key press propagates and changes the cursor
256 // location.
257 AcceptSelectedLine();
258 return false;
259 case ui::VKEY_RETURN:
260 return AcceptSelectedLine();
261 default:
262 return false;
263 }
264 }
265
OnSuggestionsChanged()266 void AutofillPopupControllerImpl::OnSuggestionsChanged() {
267 #if defined(OS_ANDROID)
268 // Assume that suggestions are (still) available. If this is wrong, the method
269 // |HideViewAndDie| will be called soon after and will hide all suggestions.
270 ManualFillingController::GetOrCreate(web_contents_)
271 ->UpdateSourceAvailability(FillingSource::AUTOFILL,
272 /*has_suggestions=*/true);
273 #endif
274
275 // Platform-specific draw call.
276 view_->OnSuggestionsChanged();
277 }
278
SelectionCleared()279 void AutofillPopupControllerImpl::SelectionCleared() {
280 SetSelectedLine(base::nullopt);
281 }
282
AcceptSuggestion(int index)283 void AutofillPopupControllerImpl::AcceptSuggestion(int index) {
284 const Suggestion& suggestion = suggestions_[index];
285 #if defined(OS_ANDROID)
286 auto mf_controller = ManualFillingController::GetOrCreate(web_contents_);
287 // Accepting a suggestion should hide all suggestions. To prevent them from
288 // coming up in Multi-Window mode, mark the source as unavailable.
289 mf_controller->UpdateSourceAvailability(FillingSource::AUTOFILL,
290 /*has_suggestions=*/false);
291 mf_controller->Hide();
292 #endif
293 delegate_->DidAcceptSuggestion(suggestion.value, suggestion.frontend_id,
294 index);
295 }
296
container_view() const297 gfx::NativeView AutofillPopupControllerImpl::container_view() const {
298 return controller_common_.container_view;
299 }
300
GetWebContents() const301 content::WebContents* AutofillPopupControllerImpl::GetWebContents() const {
302 return web_contents_;
303 }
304
element_bounds() const305 const gfx::RectF& AutofillPopupControllerImpl::element_bounds() const {
306 return controller_common_.element_bounds;
307 }
308
SetElementBounds(const gfx::RectF & bounds)309 void AutofillPopupControllerImpl::SetElementBounds(const gfx::RectF& bounds) {
310 controller_common_.element_bounds.set_origin(bounds.origin());
311 controller_common_.element_bounds.set_size(bounds.size());
312 }
313
IsRTL() const314 bool AutofillPopupControllerImpl::IsRTL() const {
315 return controller_common_.text_direction == base::i18n::RIGHT_TO_LEFT;
316 }
317
GetSuggestions() const318 std::vector<Suggestion> AutofillPopupControllerImpl::GetSuggestions() const {
319 return suggestions_;
320 }
321
GetLineCount() const322 int AutofillPopupControllerImpl::GetLineCount() const {
323 return suggestions_.size();
324 }
325
GetSuggestionAt(int row) const326 const Suggestion& AutofillPopupControllerImpl::GetSuggestionAt(int row) const {
327 return suggestions_[row];
328 }
329
GetSuggestionValueAt(int row) const330 const base::string16& AutofillPopupControllerImpl::GetSuggestionValueAt(
331 int row) const {
332 return suggestions_[row].value;
333 }
334
GetSuggestionLabelAt(int row) const335 const base::string16& AutofillPopupControllerImpl::GetSuggestionLabelAt(
336 int row) const {
337 return suggestions_[row].label;
338 }
339
GetRemovalConfirmationText(int list_index,base::string16 * title,base::string16 * body)340 bool AutofillPopupControllerImpl::GetRemovalConfirmationText(
341 int list_index,
342 base::string16* title,
343 base::string16* body) {
344 return delegate_->GetDeletionConfirmationText(
345 suggestions_[list_index].value, suggestions_[list_index].frontend_id,
346 title, body);
347 }
348
RemoveSuggestion(int list_index)349 bool AutofillPopupControllerImpl::RemoveSuggestion(int list_index) {
350 if (!delegate_->RemoveSuggestion(suggestions_[list_index].value,
351 suggestions_[list_index].frontend_id)) {
352 return false;
353 }
354
355 // Remove the deleted element.
356 suggestions_.erase(suggestions_.begin() + list_index);
357
358 selected_line_.reset();
359
360 if (HasSuggestions()) {
361 delegate_->ClearPreviewedForm();
362 OnSuggestionsChanged();
363 } else {
364 Hide(PopupHidingReason::kNoSuggestions);
365 }
366
367 return true;
368 }
369
selected_line() const370 base::Optional<int> AutofillPopupControllerImpl::selected_line() const {
371 return selected_line_;
372 }
373
GetPopupType() const374 PopupType AutofillPopupControllerImpl::GetPopupType() const {
375 return delegate_->GetPopupType();
376 }
377
SetSelectedLine(base::Optional<int> selected_line)378 void AutofillPopupControllerImpl::SetSelectedLine(
379 base::Optional<int> selected_line) {
380 if (selected_line_ == selected_line)
381 return;
382
383 if (selected_line) {
384 DCHECK_LT(*selected_line, GetLineCount());
385 if (!CanAccept(suggestions_[*selected_line].frontend_id))
386 selected_line = base::nullopt;
387 }
388
389 auto previous_selected_line(selected_line_);
390 selected_line_ = selected_line;
391 view_->OnSelectedRowChanged(previous_selected_line, selected_line_);
392
393 if (selected_line_) {
394 delegate_->DidSelectSuggestion(suggestions_[*selected_line_].value,
395 suggestions_[*selected_line_].frontend_id);
396 } else {
397 delegate_->ClearPreviewedForm();
398 }
399 }
400
SelectNextLine()401 void AutofillPopupControllerImpl::SelectNextLine() {
402 int new_selected_line = selected_line_ ? *selected_line_ + 1 : 0;
403
404 // Skip over any lines that can't be selected.
405 while (new_selected_line < GetLineCount() &&
406 !CanAccept(suggestions_[new_selected_line].frontend_id)) {
407 ++new_selected_line;
408 }
409
410 if (new_selected_line >= GetLineCount())
411 new_selected_line = 0;
412
413 SetSelectedLine(new_selected_line);
414 }
415
SelectPreviousLine()416 void AutofillPopupControllerImpl::SelectPreviousLine() {
417 int new_selected_line = selected_line_.value_or(0) - 1;
418
419 // Skip over any lines that can't be selected.
420 while (new_selected_line >= 0 &&
421 !CanAccept(GetSuggestionAt(new_selected_line).frontend_id)) {
422 --new_selected_line;
423 }
424
425 if (new_selected_line < 0)
426 new_selected_line = GetLineCount() - 1;
427
428 SetSelectedLine(new_selected_line);
429 }
430
RemoveSelectedLine()431 bool AutofillPopupControllerImpl::RemoveSelectedLine() {
432 if (!selected_line_)
433 return false;
434
435 DCHECK_LT(*selected_line_, GetLineCount());
436 return RemoveSuggestion(*selected_line_);
437 }
438
CanAccept(int id)439 bool AutofillPopupControllerImpl::CanAccept(int id) {
440 return id != POPUP_ITEM_ID_SEPARATOR &&
441 id != POPUP_ITEM_ID_INSECURE_CONTEXT_PAYMENT_DISABLED_MESSAGE &&
442 id != POPUP_ITEM_ID_MIXED_FORM_MESSAGE && id != POPUP_ITEM_ID_TITLE;
443 }
444
HasSuggestions()445 bool AutofillPopupControllerImpl::HasSuggestions() {
446 if (suggestions_.empty())
447 return false;
448 int id = suggestions_[0].frontend_id;
449 return id > 0 || id == POPUP_ITEM_ID_AUTOCOMPLETE_ENTRY ||
450 id == POPUP_ITEM_ID_PASSWORD_ENTRY ||
451 id == POPUP_ITEM_ID_USERNAME_ENTRY ||
452 id == POPUP_ITEM_ID_ACCOUNT_STORAGE_PASSWORD_ENTRY ||
453 id == POPUP_ITEM_ID_ACCOUNT_STORAGE_USERNAME_ENTRY ||
454 id == POPUP_ITEM_ID_DATALIST_ENTRY ||
455 id == POPUP_ITEM_ID_SCAN_CREDIT_CARD;
456 }
457
SetValues(const std::vector<Suggestion> & suggestions)458 void AutofillPopupControllerImpl::SetValues(
459 const std::vector<Suggestion>& suggestions) {
460 suggestions_ = suggestions;
461 }
462
GetWeakPtr()463 WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetWeakPtr() {
464 return weak_ptr_factory_.GetWeakPtr();
465 }
466
AcceptSelectedLine()467 bool AutofillPopupControllerImpl::AcceptSelectedLine() {
468 if (!selected_line_)
469 return false;
470
471 DCHECK_LT(*selected_line_, GetLineCount());
472
473 if (!CanAccept(suggestions_[*selected_line_].frontend_id))
474 return false;
475
476 AcceptSuggestion(*selected_line_);
477 return true;
478 }
479
ClearState()480 void AutofillPopupControllerImpl::ClearState() {
481 // Don't clear view_, because otherwise the popup will have to get regenerated
482 // and this will cause flickering.
483 suggestions_.clear();
484
485 selected_line_.reset();
486 }
487
HideViewAndDie()488 void AutofillPopupControllerImpl::HideViewAndDie() {
489 #if defined(OS_ANDROID)
490 // Mark the popup-like filling sources as unavailable.
491 // Note: We don't invoke ManualFillingController::Hide() here, as we might
492 // switch between text input fields.
493 ManualFillingController::GetOrCreate(web_contents_)
494 ->UpdateSourceAvailability(FillingSource::AUTOFILL,
495 /*has_suggestions=*/false);
496 #endif
497
498 if (view_) {
499 // We need to fire the event while view is not deleted yet.
500 FireControlsChangedEvent(false);
501 view_->Hide();
502 }
503
504 delete this;
505 }
506
FireControlsChangedEvent(bool is_show)507 void AutofillPopupControllerImpl::FireControlsChangedEvent(bool is_show) {
508 if (!accessibility_state_utils::IsScreenReaderEnabled())
509 return;
510 DCHECK(view_);
511
512 // Retrieve the ax tree id associated with the current web contents.
513 ui::AXTreeID tree_id = delegate_->GetAutofillDriver()->GetAxTreeId();
514
515 // Retrieve the ax node id associated with the current web contents' element
516 // that has a controller relation to the current autofill popup.
517 int32_t node_id = delegate_->GetWebContentsPopupControllerAxId();
518
519 // In order to get the AXPlatformNode for the ax node id, we first need
520 // the AXPlatformNode for the web contents.
521 ui::AXPlatformNode* root_platform_node =
522 GetRootAXPlatformNodeForWebContents();
523 if (!root_platform_node)
524 return;
525
526 ui::AXPlatformNodeDelegate* root_platform_node_delegate =
527 root_platform_node->GetDelegate();
528 if (!root_platform_node_delegate)
529 return;
530
531 // Now get the target node from its tree ID and node ID.
532 ui::AXPlatformNode* target_node =
533 root_platform_node_delegate->GetFromTreeIDAndNodeID(tree_id, node_id);
534 base::Optional<int32_t> popup_ax_id = view_->GetAxUniqueId();
535 if (!target_node || !popup_ax_id)
536 return;
537
538 // All the conditions are valid, raise the accessibility event and set global
539 // popup ax unique id.
540 if (is_show)
541 ui::SetActivePopupAxUniqueId(popup_ax_id);
542 else
543 ui::ClearActivePopupAxUniqueId();
544
545 target_node->NotifyAccessibilityEvent(ax::mojom::Event::kControlsChanged);
546 }
547
548 ui::AXPlatformNode*
GetRootAXPlatformNodeForWebContents()549 AutofillPopupControllerImpl::GetRootAXPlatformNodeForWebContents() {
550 if (!web_contents_)
551 return nullptr;
552
553 auto* rwhv = web_contents_->GetRenderWidgetHostView();
554 if (!rwhv)
555 return nullptr;
556
557 // RWHV gives us a NativeViewAccessible.
558 gfx::NativeViewAccessible native_view_accessible =
559 rwhv->GetNativeViewAccessible();
560 if (!native_view_accessible)
561 return nullptr;
562
563 // NativeViewAccessible corresponds to an AXPlatformNode.
564 return ui::AXPlatformNode::FromNativeViewAccessible(native_view_accessible);
565 }
566
567 } // namespace autofill
568