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