| // 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. |
| |
| #include "components/autofill_assistant/browser/actions/prompt_action.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/containers/flat_map.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "components/autofill_assistant/browser/actions/action_delegate.h" |
| #include "components/autofill_assistant/browser/element_precondition.h" |
| #include "components/autofill_assistant/browser/web/element.h" |
| #include "url/gurl.h" |
| |
| namespace autofill_assistant { |
| |
| PromptAction::PromptAction(ActionDelegate* delegate, const ActionProto& proto) |
| : Action(delegate, proto) { |
| DCHECK(proto_.has_prompt()); |
| } |
| |
| PromptAction::~PromptAction() {} |
| |
| bool PromptAction::ShouldInterruptOnPause() const { |
| return true; |
| } |
| |
| void PromptAction::InternalProcessAction(ProcessActionCallback callback) { |
| callback_ = std::move(callback); |
| if (proto_.prompt().choices_size() == 0) { |
| EndAction(ClientStatus(INVALID_ACTION)); |
| return; |
| } |
| |
| if (proto_.prompt().has_message()) { |
| // TODO(b/144468818): Deprecate and remove message from this action and use |
| // tell instead. |
| delegate_->SetStatusMessage(proto_.prompt().message()); |
| } |
| |
| if (proto_.prompt().browse_mode()) { |
| delegate_->SetBrowseDomainsAllowlist( |
| {proto_.prompt().browse_domains_allowlist().begin(), |
| proto_.prompt().browse_domains_allowlist().end()}); |
| } |
| |
| SetupConditions(); |
| UpdateUserActions(); |
| |
| wait_time_stopwatch_.Start(); |
| if (HasNonemptyPreconditions() || auto_select_ || |
| proto_.prompt().allow_interrupt()) { |
| delegate_->WaitForDom( |
| base::TimeDelta::Max(), proto_.prompt().allow_interrupt(), |
| /* observer= */ nullptr, |
| base::BindRepeating(&PromptAction::RegisterChecks, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&PromptAction::OnWaitForElementTimed, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::BindOnce(&PromptAction::OnDoneWaitForDom, |
| weak_ptr_factory_.GetWeakPtr()))); |
| } |
| } |
| |
| void PromptAction::RegisterChecks( |
| BatchElementChecker* checker, |
| base::OnceCallback<void(const ClientStatus&)> wait_for_dom_callback) { |
| last_period_stopwatch_.Stop(); |
| last_checks_stopwatch_.Reset(); |
| last_checks_stopwatch_.Start(); |
| if (!callback_) { |
| // Action is done; checks aren't necessary anymore. |
| std::move(wait_for_dom_callback).Run(OkClientStatus()); |
| return; |
| } |
| |
| UpdateUserActions(); |
| |
| for (size_t i = 0; i < preconditions_.size(); i++) { |
| preconditions_[i]->Check(checker, |
| base::BindOnce(&PromptAction::OnPreconditionResult, |
| weak_ptr_factory_.GetWeakPtr(), i)); |
| } |
| |
| if (auto_select_) { |
| auto_select_->Check(checker, |
| base::BindOnce(&PromptAction::OnAutoSelectCondition, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| checker->AddAllDoneCallback(base::BindOnce(&PromptAction::OnElementChecksDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(wait_for_dom_callback))); |
| } |
| |
| void PromptAction::SetupConditions() { |
| int choice_count = proto_.prompt().choices_size(); |
| preconditions_.resize(choice_count); |
| precondition_results_.resize(choice_count); |
| positive_precondition_changes_.resize(choice_count); |
| precondition_stopwatches_.resize(choice_count); |
| |
| for (int i = 0; i < choice_count; i++) { |
| auto& choice_proto = proto_.prompt().choices(i); |
| preconditions_[i] = |
| std::make_unique<ElementPrecondition>(choice_proto.show_only_when()); |
| precondition_results_[i] = preconditions_[i]->empty(); |
| positive_precondition_changes_[i] = false; |
| } |
| |
| ElementConditionsProto auto_select; |
| for (int i = 0; i < choice_count; i++) { |
| auto& choice_proto = proto_.prompt().choices(i); |
| if (choice_proto.has_auto_select_when()) { |
| ElementConditionProto* condition = auto_select.add_conditions(); |
| *condition = choice_proto.auto_select_when(); |
| condition->set_payload(base::NumberToString(i)); |
| } |
| } |
| if (!auto_select.conditions().empty()) { |
| ElementConditionProto auto_select_condition; |
| *auto_select_condition.mutable_any_of() = auto_select; |
| auto_select_ = std::make_unique<ElementPrecondition>(auto_select_condition); |
| } |
| } |
| |
| bool PromptAction::HasNonemptyPreconditions() { |
| for (const auto& precondition : preconditions_) { |
| if (!precondition->empty()) |
| return true; |
| } |
| return false; |
| } |
| |
| void PromptAction::OnPreconditionResult( |
| size_t choice_index, |
| const ClientStatus& status, |
| const std::vector<std::string>& ignored_payloads, |
| const base::flat_map<std::string, DomObjectFrameStack>& ignored_elements) { |
| bool precondition_is_met = status.ok(); |
| if (precondition_results_[choice_index] == precondition_is_met) |
| return; |
| |
| precondition_results_[choice_index] = precondition_is_met; |
| positive_precondition_changes_[choice_index] = precondition_is_met; |
| precondition_changed_ = true; |
| } |
| |
| void PromptAction::UpdateUserActions() { |
| DCHECK(callback_); // Make sure we're still waiting for a response |
| |
| auto user_actions = std::make_unique<std::vector<UserAction>>(); |
| for (int i = 0; i < proto_.prompt().choices_size(); i++) { |
| auto& choice_proto = proto_.prompt().choices(i); |
| UserAction user_action(choice_proto.chip(), choice_proto.direct_action(), |
| /* enabled = */ true, |
| /* identifier = */ std::string()); |
| if (!user_action.has_triggers()) |
| continue; |
| |
| // Hide actions whose preconditions don't match. |
| if (!precondition_results_[i] && !choice_proto.allow_disabling()) |
| continue; |
| |
| user_action.SetEnabled(precondition_results_[i]); |
| user_action.SetCallback(base::BindOnce(&PromptAction::OnSuggestionChosen, |
| weak_ptr_factory_.GetWeakPtr(), i)); |
| user_actions->emplace_back(std::move(user_action)); |
| } |
| base::OnceCallback<void()> end_on_navigation_callback; |
| if (proto_.prompt().end_on_navigation()) { |
| end_on_navigation_callback = base::BindOnce( |
| &PromptAction::OnNavigationEnded, weak_ptr_factory_.GetWeakPtr()); |
| } |
| delegate_->Prompt( |
| std::move(user_actions), proto_.prompt().disable_force_expand_sheet(), |
| std::move(end_on_navigation_callback), proto_.prompt().browse_mode(), |
| proto_.prompt().browse_mode_invisible()); |
| precondition_changed_ = false; |
| } |
| |
| void PromptAction::UpdateTimings() { |
| for (int i = 0; i < proto_.prompt().choices_size(); i++) { |
| if (!precondition_results_[i]) { |
| precondition_stopwatches_[i].Reset(); |
| } else if (positive_precondition_changes_[i]) { |
| positive_precondition_changes_[i] = false; |
| // The precondition now contains the duration of the wait for dom |
| // operation retry period plus the time that it took to perform the actual |
| // checks. We want to instead record only half of the period plus the |
| // checks. |
| precondition_stopwatches_[i].AddTime(last_checks_stopwatch_); |
| precondition_stopwatches_[i].AddTime( |
| last_period_stopwatch_.TotalElapsed() / 2); |
| } |
| } |
| } |
| |
| void PromptAction::OnAutoSelectCondition( |
| const ClientStatus& status, |
| const std::vector<std::string>& payloads, |
| const base::flat_map<std::string, DomObjectFrameStack>& ignored_elements) { |
| if (payloads.empty()) |
| return; |
| |
| // We want to select the first matching choice, so only the first entry of |
| // payloads matter. |
| base::StringToInt(payloads[0], &auto_select_choice_index_); |
| |
| // Calling OnSuggestionChosen() is delayed until try_done, as it indirectly |
| // deletes the batch element checker, which isn't supported from an element |
| // check callback. |
| } |
| |
| void PromptAction::OnElementChecksDone( |
| base::OnceCallback<void(const ClientStatus&)> wait_for_dom_callback) { |
| last_checks_stopwatch_.Stop(); |
| if (precondition_changed_) { |
| UpdateUserActions(); |
| } |
| |
| UpdateTimings(); |
| last_period_stopwatch_.Reset(); |
| last_period_stopwatch_.Start(); |
| |
| // Calling wait_for_dom_callback with successful status is a way of asking the |
| // WaitForDom to end gracefully and call OnDoneWaitForDom with the status. |
| // Note that it is possible for WaitForDom to decide not to call |
| // OnDoneWaitForDom, if an interrupt triggers at the same time, so we cannot |
| // cancel the prompt and choose the suggestion just yet. |
| if (auto_select_choice_index_ >= 0) { |
| std::move(wait_for_dom_callback).Run(OkClientStatus()); |
| return; |
| } |
| |
| // Let WaitForDom know we're still waiting for an element. |
| std::move(wait_for_dom_callback).Run(ClientStatus(ELEMENT_RESOLUTION_FAILED)); |
| } |
| |
| void PromptAction::OnDoneWaitForDom(const ClientStatus& status) { |
| if (!callback_) { |
| return; |
| } |
| // Status comes either from AutoSelectDone(), from checking the selector, or |
| // from an interrupt failure. Special-case the AutoSelectDone() case. |
| if (auto_select_choice_index_ >= 0) { |
| OnSuggestionChosen(auto_select_choice_index_); |
| return; |
| } |
| // Everything else should be forwarded. |
| EndAction(status); |
| } |
| |
| void PromptAction::OnSuggestionChosen(int choice_index) { |
| if (!callback_) { |
| NOTREACHED(); |
| return; |
| } |
| |
| if (auto_select_choice_index_ < 0) { |
| wait_time_stopwatch_.Stop(); |
| action_stopwatch_.TransferToWaitTime(wait_time_stopwatch_.TotalElapsed()); |
| action_stopwatch_.TransferToActiveTime( |
| precondition_stopwatches_[choice_index].TotalElapsed()); |
| } |
| |
| DCHECK(choice_index >= 0 && choice_index <= proto_.prompt().choices_size()); |
| |
| processed_action_proto_->mutable_prompt_choice()->set_server_payload( |
| proto_.prompt().choices(choice_index).server_payload()); |
| EndAction(ClientStatus(ACTION_APPLIED)); |
| } |
| |
| void PromptAction::OnNavigationEnded() { |
| if (!callback_) { |
| NOTREACHED(); |
| return; |
| } |
| action_stopwatch_.TransferToWaitTime(wait_time_stopwatch_.TotalElapsed()); |
| |
| processed_action_proto_->mutable_prompt_choice()->set_navigation_ended(true); |
| EndAction(ClientStatus(ACTION_APPLIED)); |
| } |
| |
| void PromptAction::EndAction(const ClientStatus& status) { |
| delegate_->CleanUpAfterPrompt(); |
| // Clear the allowlist when a browse action is done. |
| if (proto_.prompt().browse_mode()) { |
| delegate_->SetBrowseDomainsAllowlist({}); |
| } |
| UpdateProcessedAction(status); |
| std::move(callback_).Run(std::move(processed_action_proto_)); |
| } |
| |
| } // namespace autofill_assistant |