blob: 0355b1d0c01609cbd58b013592c5dc75540b11ec [file] [log] [blame]
// 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/controller.h"
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/tick_clock.h"
#include "base/values.h"
#include "components/autofill_assistant/browser/actions/collect_user_data_action.h"
#include "components/autofill_assistant/browser/controller_observer.h"
#include "components/autofill_assistant/browser/features.h"
#include "components/autofill_assistant/browser/metrics.h"
#include "components/autofill_assistant/browser/protocol_utils.h"
#include "components/autofill_assistant/browser/service/service_impl.h"
#include "components/autofill_assistant/browser/trigger_context.h"
#include "components/autofill_assistant/browser/url_utils.h"
#include "components/autofill_assistant/browser/user_data.h"
#include "components/autofill_assistant/browser/user_data_util.h"
#include "components/autofill_assistant/browser/view_layout.pb.h"
#include "components/autofill_assistant/browser/web/element_store.h"
#include "components/google/core/common/google_util.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "net/http/http_status_code.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace autofill_assistant {
namespace {
// The initial progress to set when autostarting and showing the "Loading..."
// message.
static constexpr int kAutostartInitialProgress = 5;
// Experiment for toggling the new progress bar.
const char kProgressBarExperiment[] = "4400697";
} // namespace
Controller::Controller(content::WebContents* web_contents,
Client* client,
const base::TickClock* tick_clock,
base::WeakPtr<RuntimeManagerImpl> runtime_manager,
std::unique_ptr<Service> service)
: content::WebContentsObserver(web_contents),
client_(client),
tick_clock_(tick_clock),
runtime_manager_(runtime_manager),
service_(service ? std::move(service)
: ServiceImpl::Create(web_contents->GetBrowserContext(),
client_)),
user_data_(std::make_unique<UserData>()),
navigating_to_new_document_(web_contents->IsWaitingForResponse()) {
user_model_.AddObserver(this);
}
Controller::~Controller() {
user_model_.RemoveObserver(this);
}
Controller::DetailsHolder::DetailsHolder(
std::unique_ptr<Details> details,
std::unique_ptr<base::OneShotTimer> timer)
: details_(std::move(details)), timer_(std::move(timer)) {}
Controller::DetailsHolder::~DetailsHolder() = default;
Controller::DetailsHolder::DetailsHolder(DetailsHolder&& other) = default;
Controller::DetailsHolder& Controller::DetailsHolder::operator=(
DetailsHolder&& other) = default;
const Details& Controller::DetailsHolder::GetDetails() const {
return *details_;
}
bool Controller::DetailsHolder::CurrentlyVisible() const {
// If there is a timer associated to these details, then they should be shown
// only once the timer has triggered.
return !timer_;
}
void Controller::DetailsHolder::Enable() {
timer_.reset();
}
const ClientSettings& Controller::GetSettings() {
return settings_;
}
const GURL& Controller::GetCurrentURL() {
const GURL& last_committed = web_contents()->GetLastCommittedURL();
if (!last_committed.is_empty())
return last_committed;
return deeplink_url_;
}
const GURL& Controller::GetDeeplinkURL() {
return deeplink_url_;
}
const GURL& Controller::GetScriptURL() {
return script_url_;
}
Service* Controller::GetService() {
return service_.get();
}
WebController* Controller::GetWebController() {
if (!web_controller_) {
web_controller_ = WebController::CreateForWebContents(web_contents());
}
return web_controller_.get();
}
ElementStore* Controller::GetElementStore() const {
if (!element_store_) {
element_store_ = std::make_unique<ElementStore>(web_contents());
}
return element_store_.get();
}
const TriggerContext* Controller::GetTriggerContext() {
DCHECK(trigger_context_);
return trigger_context_.get();
}
autofill::PersonalDataManager* Controller::GetPersonalDataManager() {
return client_->GetPersonalDataManager();
}
WebsiteLoginManager* Controller::GetWebsiteLoginManager() {
return client_->GetWebsiteLoginManager();
}
content::WebContents* Controller::GetWebContents() {
return web_contents();
}
std::string Controller::GetEmailAddressForAccessTokenAccount() {
return client_->GetEmailAddressForAccessTokenAccount();
}
std::string Controller::GetLocale() {
return client_->GetLocale();
}
void Controller::SetTouchableElementArea(const ElementAreaProto& area) {
touchable_element_area()->SetFromProto(area);
}
void Controller::SetStatusMessage(const std::string& message) {
status_message_ = message;
for (ControllerObserver& observer : observers_) {
observer.OnStatusMessageChanged(message);
}
}
std::string Controller::GetStatusMessage() const {
return status_message_;
}
void Controller::SetBubbleMessage(const std::string& message) {
bubble_message_ = message;
for (ControllerObserver& observer : observers_) {
observer.OnBubbleMessageChanged(message);
}
}
std::string Controller::GetBubbleMessage() const {
return bubble_message_;
}
void Controller::SetDetails(std::unique_ptr<Details> details,
base::TimeDelta delay) {
details_.clear();
// There is nothing to append: notify that we cleared the details and return.
if (!details) {
NotifyDetailsChanged();
return;
}
// If there is a delay, notify now that details have been cleared. If there is
// no delay, AppendDetails will take care of the notifying the observers after
// appending the details.
if (!delay.is_zero()) {
NotifyDetailsChanged();
}
AppendDetails(std::move(details), delay);
}
void Controller::AppendDetails(std::unique_ptr<Details> details,
base::TimeDelta delay) {
if (!details) {
return;
}
if (delay.is_zero()) {
details_.push_back(DetailsHolder(std::move(details), /* timer= */ nullptr));
NotifyDetailsChanged();
return;
}
// Delay the addition of the new details.
size_t details_index = details_.size();
auto timer = std::make_unique<base::OneShotTimer>();
timer->Start(FROM_HERE, delay,
base::BindOnce(&Controller::MakeDetailsVisible,
weak_ptr_factory_.GetWeakPtr(), details_index));
details_.push_back(DetailsHolder(std::move(details), std::move(timer)));
}
void Controller::MakeDetailsVisible(size_t details_index) {
if (details_index < details_.size()) {
details_[details_index].Enable();
NotifyDetailsChanged();
}
}
void Controller::NotifyDetailsChanged() {
std::vector<Details> details = GetDetails();
for (ControllerObserver& observer : observers_) {
observer.OnDetailsChanged(details);
}
}
std::vector<Details> Controller::GetDetails() const {
std::vector<Details> details;
for (const auto& holder : details_) {
if (holder.CurrentlyVisible()) {
details.push_back(holder.GetDetails());
}
}
return details;
}
int Controller::GetProgress() const {
return progress_;
}
absl::optional<int> Controller::GetProgressActiveStep() const {
return progress_active_step_;
}
absl::optional<ShowProgressBarProto::StepProgressBarConfiguration>
Controller::GetStepProgressBarConfiguration() const {
return step_progress_bar_configuration_;
}
void Controller::SetInfoBox(const InfoBox& info_box) {
if (!info_box_) {
info_box_ = std::make_unique<InfoBox>();
}
*info_box_ = info_box;
for (ControllerObserver& observer : observers_) {
observer.OnInfoBoxChanged(info_box_.get());
}
}
void Controller::ClearInfoBox() {
info_box_.reset();
for (ControllerObserver& observer : observers_) {
observer.OnInfoBoxChanged(nullptr);
}
}
const InfoBox* Controller::GetInfoBox() const {
return info_box_.get();
}
void Controller::SetProgress(int progress) {
// Progress can only increase.
if (progress_ >= progress)
return;
progress_ = progress;
for (ControllerObserver& observer : observers_) {
observer.OnProgressChanged(progress);
}
}
bool Controller::SetProgressActiveStepIdentifier(
const std::string& active_step_identifier) {
if (!step_progress_bar_configuration_.has_value()) {
return false;
}
auto it = std::find_if(
step_progress_bar_configuration_->annotated_step_icons().cbegin(),
step_progress_bar_configuration_->annotated_step_icons().cend(),
[&](const ShowProgressBarProto::StepProgressBarIcon& icon) {
return icon.identifier() == active_step_identifier;
});
if (it == step_progress_bar_configuration_->annotated_step_icons().cend()) {
return false;
}
SetProgressActiveStep(std::distance(
step_progress_bar_configuration_->annotated_step_icons().cbegin(), it));
return true;
}
void Controller::SetProgressActiveStep(int active_step) {
if (!step_progress_bar_configuration_.has_value()) {
return;
}
// Default step progress bar has 2 steps.
int max_step = std::max(
2, step_progress_bar_configuration_->annotated_step_icons().size());
int new_active_step = active_step;
if (active_step < 0 || active_step > max_step) {
new_active_step = max_step;
}
// Step can only increase.
if (progress_active_step_.has_value() &&
*progress_active_step_ >= new_active_step) {
return;
}
progress_active_step_ = new_active_step;
for (ControllerObserver& observer : observers_) {
observer.OnProgressActiveStepChanged(new_active_step);
}
}
void Controller::SetProgressVisible(bool visible) {
if (progress_visible_ == visible)
return;
progress_visible_ = visible;
for (ControllerObserver& observer : observers_) {
observer.OnProgressVisibilityChanged(visible);
}
}
bool Controller::GetProgressVisible() const {
return progress_visible_;
}
void Controller::SetStepProgressBarConfiguration(
const ShowProgressBarProto::StepProgressBarConfiguration& configuration) {
step_progress_bar_configuration_ = configuration;
if (!configuration.annotated_step_icons().empty() &&
progress_active_step_.has_value() &&
configuration.annotated_step_icons().size() < *progress_active_step_) {
progress_active_step_ = configuration.annotated_step_icons().size();
}
for (ControllerObserver& observer : observers_) {
observer.OnStepProgressBarConfigurationChanged(configuration);
if (progress_active_step_.has_value()) {
observer.OnProgressActiveStepChanged(*progress_active_step_);
}
observer.OnProgressBarErrorStateChanged(progress_bar_error_state_);
}
}
void Controller::SetProgressBarErrorState(bool error) {
if (progress_bar_error_state_ == error) {
return;
}
progress_bar_error_state_ = error;
for (ControllerObserver& observer : observers_) {
observer.OnProgressBarErrorStateChanged(error);
}
}
bool Controller::GetProgressBarErrorState() const {
return progress_bar_error_state_;
}
const std::vector<UserAction>& Controller::GetUserActions() const {
static const base::NoDestructor<std::vector<UserAction>> no_user_actions_;
return user_actions_ ? *user_actions_ : *no_user_actions_;
}
void Controller::SetUserActions(
std::unique_ptr<std::vector<UserAction>> user_actions) {
if (user_actions) {
SetDefaultChipType(user_actions.get());
}
user_actions_ = std::move(user_actions);
SetVisibilityAndUpdateUserActions();
}
void Controller::SetVisibilityAndUpdateUserActions() {
// All non-cancel chips should be hidden while the keyboard is showing.
if (user_actions_) {
for (UserAction& user_action : *user_actions_) {
if (user_action.chip().type != CANCEL_ACTION) {
user_action.chip().visible = !is_keyboard_showing_;
}
}
}
for (ControllerObserver& observer : observers_) {
observer.OnUserActionsChanged(GetUserActions());
}
}
bool Controller::IsNavigatingToNewDocument() {
return navigating_to_new_document_;
}
bool Controller::HasNavigationError() {
return navigation_error_;
}
void Controller::RequireUI() {
if (ui_shown_)
return;
needs_ui_ = true;
client_->AttachUI();
}
void Controller::SetUiShown(bool shown) {
ui_shown_ = shown;
if (runtime_manager_) {
runtime_manager_->SetUIState(shown ? UIState::kShown : UIState::kNotShown);
}
}
void Controller::SetGenericUi(
std::unique_ptr<GenericUserInterfaceProto> generic_ui,
base::OnceCallback<void(const ClientStatus&)> end_action_callback,
base::OnceCallback<void(const ClientStatus&)>
view_inflation_finished_callback) {
generic_user_interface_ = std::move(generic_ui);
basic_interactions_.SetEndActionCallback(std::move(end_action_callback));
basic_interactions_.SetViewInflationFinishedCallback(
std::move(view_inflation_finished_callback));
for (ControllerObserver& observer : observers_) {
observer.OnGenericUserInterfaceChanged(generic_user_interface_.get());
}
}
void Controller::SetPersistentGenericUi(
std::unique_ptr<GenericUserInterfaceProto> generic_ui,
base::OnceCallback<void(const ClientStatus&)>
view_inflation_finished_callback) {
persistent_generic_user_interface_ = std::move(generic_ui);
basic_interactions_.SetPersistentViewInflationFinishedCallback(
std::move(view_inflation_finished_callback));
for (ControllerObserver& observer : observers_) {
observer.OnPersistentGenericUserInterfaceChanged(
persistent_generic_user_interface_.get());
}
}
void Controller::ClearGenericUi() {
generic_user_interface_.reset();
basic_interactions_.ClearCallbacks();
for (ControllerObserver& observer : observers_) {
observer.OnGenericUserInterfaceChanged(nullptr);
}
}
void Controller::ClearPersistentGenericUi() {
persistent_generic_user_interface_.reset();
basic_interactions_.ClearPersistentUiCallbacks();
for (ControllerObserver& observer : observers_) {
observer.OnPersistentGenericUserInterfaceChanged(nullptr);
}
}
void Controller::SetBrowseModeInvisible(bool invisible) {
browse_mode_invisible_ = invisible;
}
bool Controller::ShouldShowWarning() {
return state_ == AutofillAssistantState::RUNNING ||
state_ == AutofillAssistantState::PROMPT;
}
void Controller::SetShowFeedbackChip(bool show_feedback_chip) {
show_feedback_chip_on_graceful_shutdown_ = show_feedback_chip;
}
void Controller::AddNavigationListener(
ScriptExecutorDelegate::NavigationListener* listener) {
navigation_listeners_.AddObserver(listener);
}
void Controller::RemoveNavigationListener(
ScriptExecutorDelegate::NavigationListener* listener) {
navigation_listeners_.RemoveObserver(listener);
}
void Controller::AddListener(ScriptExecutorDelegate::Listener* listener) {
listeners_.AddObserver(listener);
}
void Controller::RemoveListener(ScriptExecutorDelegate::Listener* listener) {
listeners_.RemoveObserver(listener);
}
void Controller::SetExpandSheetForPromptAction(bool expand) {
expand_sheet_for_prompt_action_ = expand;
}
void Controller::SetBrowseDomainsAllowlist(std::vector<std::string> domains) {
browse_domains_allowlist_ = std::move(domains);
}
bool Controller::PerformUserActionWithContext(
int index,
std::unique_ptr<TriggerContext> context) {
if (!user_actions_ || index < 0 ||
static_cast<size_t>(index) >= user_actions_->size()) {
NOTREACHED() << "Invalid user action index: " << index;
return false;
}
if (!(*user_actions_)[index].enabled()) {
NOTREACHED() << "Action at index " << index << " is disabled.";
return false;
}
UserAction user_action = std::move((*user_actions_)[index]);
SetUserActions(nullptr);
user_action.Call(std::move(context));
event_handler_.DispatchEvent(
{EventProto::kOnUserActionCalled, user_action.identifier()});
return true;
}
void Controller::SetViewportMode(ViewportMode mode) {
if (mode == viewport_mode_)
return;
viewport_mode_ = mode;
for (ControllerObserver& observer : observers_) {
observer.OnViewportModeChanged(mode);
}
}
void Controller::SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) {
if (peek_mode == peek_mode_)
return;
peek_mode_ = peek_mode;
for (ControllerObserver& observer : observers_) {
observer.OnPeekModeChanged(peek_mode);
}
}
void Controller::ExpandBottomSheet() {
for (ControllerObserver& observer : observers_) {
// TODO(crbug/806868): The interface here and in some of the other On*
// events should be coming from the UI layer, not the controller. Or at
// least be renamed to something like On*Requested.
observer.OnExpandBottomSheet();
}
}
void Controller::CollapseBottomSheet() {
for (ControllerObserver& observer : observers_) {
// TODO(crbug/806868): The interface here and in some of the other On*
// events should be coming from the UI layer, not the controller. Or at
// least be renamed to something like On*Requested.
observer.OnCollapseBottomSheet();
}
}
const FormProto* Controller::GetForm() const {
return form_.get();
}
const FormProto::Result* Controller::GetFormResult() const {
return form_result_.get();
}
bool Controller::SetForm(
std::unique_ptr<FormProto> form,
base::RepeatingCallback<void(const FormProto::Result*)> changed_callback,
base::OnceCallback<void(const ClientStatus&)> cancel_callback) {
form_.reset();
form_result_.reset();
form_changed_callback_ = base::DoNothing();
form_cancel_callback_ = base::DoNothing::Once<const ClientStatus&>();
if (!form) {
for (ControllerObserver& observer : observers_) {
observer.OnFormChanged(nullptr, nullptr);
}
return true;
}
// Initialize form result. This will return false if the form is invalid or
// contains unsupported inputs.
auto form_result = std::make_unique<FormProto::Result>();
for (FormInputProto& input : *form->mutable_inputs()) {
FormInputProto::Result* result = form_result->add_input_results();
switch (input.input_type_case()) {
case FormInputProto::InputTypeCase::kCounter:
// Add the initial value of each counter into the form result.
for (const CounterInputProto::Counter& counter :
input.counter().counters()) {
result->mutable_counter()->add_values(counter.initial_value());
}
break;
case FormInputProto::InputTypeCase::kSelection: {
// Add the initial selected state of each choice into the form result.
bool has_selected = false;
for (const SelectionInputProto::Choice& choice :
input.selection().choices()) {
if (choice.selected()) {
if (has_selected && !input.selection().allow_multiple()) {
// Multiple choices are initially selected even though it is not
// allowed by the input.
return false;
}
has_selected = true;
}
result->mutable_selection()->add_selected(choice.selected());
}
break;
}
case FormInputProto::InputTypeCase::INPUT_TYPE_NOT_SET:
VLOG(1) << "Encountered input with INPUT_TYPE_NOT_SET";
return false;
// Intentionally no default case to make compilation fail if a new value
// was added to the enum but not to this list.
}
}
// Form is valid.
form_ = std::move(form);
form_result_ = std::move(form_result);
form_changed_callback_ = changed_callback;
form_cancel_callback_ = std::move(cancel_callback);
// Call the callback with initial result.
form_changed_callback_.Run(form_result_.get());
for (ControllerObserver& observer : observers_) {
observer.OnFormChanged(form_.get(), form_result_.get());
}
return true;
}
void Controller::SetCounterValue(int input_index,
int counter_index,
int value) {
if (!form_result_ || input_index < 0 ||
input_index >= form_result_->input_results_size()) {
NOTREACHED() << "Invalid input index: " << input_index;
return;
}
FormInputProto::Result* input_result =
form_result_->mutable_input_results(input_index);
if (!input_result->has_counter() || counter_index < 0 ||
counter_index >= input_result->counter().values_size()) {
NOTREACHED() << "Invalid counter index: " << counter_index;
return;
}
input_result->mutable_counter()->set_values(counter_index, value);
form_changed_callback_.Run(form_result_.get());
}
void Controller::SetChoiceSelected(int input_index,
int choice_index,
bool selected) {
if (!form_result_ || input_index < 0 ||
input_index >= form_result_->input_results_size()) {
NOTREACHED() << "Invalid input index: " << input_index;
return;
}
FormInputProto::Result* input_result =
form_result_->mutable_input_results(input_index);
if (!input_result->has_selection() || choice_index < 0 ||
choice_index >= input_result->selection().selected_size()) {
NOTREACHED() << "Invalid choice index: " << choice_index;
return;
}
input_result->mutable_selection()->set_selected(choice_index, selected);
form_changed_callback_.Run(form_result_.get());
}
UserModel* Controller::GetUserModel() {
return &user_model_;
}
EventHandler* Controller::GetEventHandler() {
return &event_handler_;
}
bool Controller::ShouldPromptActionExpandSheet() const {
return expand_sheet_for_prompt_action_;
}
BasicInteractions* Controller::GetBasicInteractions() {
return &basic_interactions_;
}
const GenericUserInterfaceProto* Controller::GetGenericUiProto() const {
return generic_user_interface_.get();
}
const GenericUserInterfaceProto* Controller::GetPersistentGenericUiProto()
const {
return persistent_generic_user_interface_.get();
}
void Controller::AddObserver(ControllerObserver* observer) {
observers_.AddObserver(observer);
}
void Controller::RemoveObserver(const ControllerObserver* observer) {
observers_.RemoveObserver(observer);
}
void Controller::DispatchEvent(const EventHandler::EventKey& key) {
event_handler_.DispatchEvent(key);
}
ViewportMode Controller::GetViewportMode() {
return viewport_mode_;
}
ConfigureBottomSheetProto::PeekMode Controller::GetPeekMode() {
return peek_mode_;
}
BottomSheetState Controller::GetBottomSheetState() {
return bottom_sheet_state_;
}
void Controller::SetBottomSheetState(BottomSheetState state) {
bottom_sheet_state_ = state;
}
bool Controller::IsTabSelected() {
return tab_selected_;
}
void Controller::SetTabSelected(bool selected) {
tab_selected_ = selected;
}
void Controller::SetOverlayColors(std::unique_ptr<OverlayColors> colors) {
overlay_colors_ = std::move(colors);
if (overlay_colors_) {
for (ControllerObserver& observer : observers_) {
observer.OnOverlayColorsChanged(*overlay_colors_);
}
} else {
OverlayColors default_colors;
for (ControllerObserver& observer : observers_) {
observer.OnOverlayColorsChanged(default_colors);
}
}
}
void Controller::GetOverlayColors(OverlayColors* colors) const {
if (!overlay_colors_)
return;
*colors = *overlay_colors_;
}
const ClientSettings& Controller::GetClientSettings() const {
return settings_;
}
void Controller::ShutdownIfNecessary() {
if (!tracking_) {
// We expect the DropOutReason to be already reported when we reach this
// point and therefore the reason we pass here in the argument should be
// ignored.
client_->Shutdown(Metrics::DropOutReason::UI_CLOSED_UNEXPECTEDLY);
}
}
void Controller::ReportNavigationStateChanged() {
for (auto& listener : navigation_listeners_) {
listener.OnNavigationStateChanged();
}
}
void Controller::EnterStoppedState(bool show_feedback_chip) {
if (script_tracker_)
script_tracker_->StopScript();
std::unique_ptr<std::vector<UserAction>> final_actions;
if (base::FeatureList::IsEnabled(features::kAutofillAssistantFeedbackChip) &&
show_feedback_chip) {
final_actions = std::make_unique<std::vector<UserAction>>();
UserAction feedback_action;
Chip feedback_chip;
feedback_chip.type = FEEDBACK_ACTION;
feedback_chip.text =
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_SEND_FEEDBACK);
feedback_action.SetCallback(base::BindOnce(&Controller::ShutdownIfNecessary,
weak_ptr_factory_.GetWeakPtr()));
feedback_action.chip() = feedback_chip;
final_actions->emplace_back(std::move(feedback_action));
}
ClearInfoBox();
SetDetails(nullptr, base::TimeDelta());
SetUserActions(std::move(final_actions));
SetCollectUserDataOptions(nullptr);
SetForm(nullptr, base::DoNothing(), base::DoNothing());
EnterState(AutofillAssistantState::STOPPED);
}
bool Controller::EnterState(AutofillAssistantState state) {
if (state_ == state)
return false;
VLOG(2) << __func__ << ": " << state_ << " -> " << state;
// The only valid way of leaving the STOPPED state is to go back to tracking
// mode - or going back to RUNNING if it was a recoverable STOPPED state.
DCHECK(
state_ != AutofillAssistantState::STOPPED ||
(state == AutofillAssistantState::TRACKING && tracking_) ||
(state == AutofillAssistantState::RUNNING && can_recover_from_stopped_));
if (state_ == AutofillAssistantState::STOPPED) {
can_recover_from_stopped_ = false;
}
state_ = state;
for (ControllerObserver& observer : observers_) {
observer.OnStateChanged(state);
}
if (!ui_shown_ && StateNeedsUI(state)) {
RequireUI();
} else if (needs_ui_ && state == AutofillAssistantState::TRACKING) {
needs_ui_ = false;
} else if (browse_mode_invisible_ && ui_shown_ &&
state == AutofillAssistantState::BROWSE) {
needs_ui_ = false;
client_->DestroyUI();
}
if (ShouldCheckScripts()) {
GetOrCheckScripts();
} else {
StopPeriodicScriptChecks();
}
return true;
}
void Controller::SetOverlayBehavior(
ConfigureUiStateProto::OverlayBehavior overlay_behavior) {
overlay_behavior_ = overlay_behavior;
for (ControllerObserver& observer : observers_) {
observer.OnShouldShowOverlayChanged(ShouldShowOverlay());
}
}
void Controller::SetWebControllerForTest(
std::unique_ptr<WebController> web_controller) {
web_controller_ = std::move(web_controller);
}
void Controller::OnUrlChange() {
if (state_ == AutofillAssistantState::STOPPED) {
PerformDelayedShutdownIfNecessary();
return;
}
user_model_.SetCurrentURL(GetCurrentURL());
GetOrCheckScripts();
}
bool Controller::ShouldCheckScripts() {
return state_ == AutofillAssistantState::TRACKING ||
state_ == AutofillAssistantState::STARTING ||
state_ == AutofillAssistantState::AUTOSTART_FALLBACK_PROMPT ||
((state_ == AutofillAssistantState::PROMPT ||
state_ == AutofillAssistantState::BROWSE) &&
(!script_tracker_ || !script_tracker_->running()));
}
void Controller::GetOrCheckScripts() {
if (!ShouldCheckScripts())
return;
const GURL& url = GetCurrentURL();
if (script_url_.host() != url.host()) {
StopPeriodicScriptChecks();
script_url_ = url;
#ifdef NDEBUG
VLOG(2) << "GetScripts for <redacted>";
#else
VLOG(2) << "GetScripts for " << script_url_.host();
#endif
GetService()->GetScriptsForUrl(
url, *trigger_context_,
base::BindOnce(&Controller::OnGetScripts, base::Unretained(this), url));
} else {
script_tracker()->CheckScripts();
StartPeriodicScriptChecks();
}
}
void Controller::StartPeriodicScriptChecks() {
periodic_script_check_count_ = settings_.periodic_script_check_count;
// If periodic checks are running, setting periodic_script_check_count_ keeps
// them running longer.
if (periodic_script_check_scheduled_)
return;
periodic_script_check_scheduled_ = true;
content::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(&Controller::OnPeriodicScriptCheck,
weak_ptr_factory_.GetWeakPtr()),
settings_.periodic_script_check_interval);
}
void Controller::StopPeriodicScriptChecks() {
periodic_script_check_count_ = 0;
}
void Controller::OnPeriodicScriptCheck() {
if (periodic_script_check_count_ > 0) {
periodic_script_check_count_--;
}
if (periodic_script_check_count_ <= 0 && !allow_autostart()) {
DCHECK_EQ(0, periodic_script_check_count_);
periodic_script_check_scheduled_ = false;
return;
}
if (allow_autostart() && !autostart_timeout_script_path_.empty() &&
tick_clock_->NowTicks() >= absolute_autostart_timeout_) {
VLOG(1) << __func__ << " giving up waiting on autostart.";
std::string script_path = autostart_timeout_script_path_;
autostart_timeout_script_path_.clear();
periodic_script_check_scheduled_ = false;
ExecuteScript(script_path, /* start_message= */ "", /* needs_ui= */ false,
std::make_unique<TriggerContext>(), state_);
return;
}
script_tracker()->CheckScripts();
content::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(&Controller::OnPeriodicScriptCheck,
weak_ptr_factory_.GetWeakPtr()),
settings_.periodic_script_check_interval);
}
void Controller::OnGetScripts(const GURL& url,
int http_status,
const std::string& response) {
if (state_ == AutofillAssistantState::STOPPED)
return;
// If the domain of the current URL changed since the request was sent, the
// response is not relevant anymore and can be safely discarded.
if (script_url_.host() != url.host())
return;
if (http_status != net::HTTP_OK) {
#ifdef NDEBUG
VLOG(1) << "Failed to get assistant scripts for <redacted>, http-status="
<< http_status;
#else
VLOG(1) << "Failed to get assistant scripts for " << script_url_.host()
<< ", http-status=" << http_status;
#endif
OnFatalError(l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR),
/*show_feedback_chip=*/true,
Metrics::DropOutReason::GET_SCRIPTS_FAILED);
return;
}
SupportsScriptResponseProto response_proto;
if (!response_proto.ParseFromString(response)) {
#ifdef NDEBUG
VLOG(2) << __func__ << " from <redacted> returned unparseable response";
#else
VLOG(2) << __func__ << " from " << script_url_.host() << " returned "
<< "unparseable response";
#endif
OnFatalError(l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR),
/*show_feedback_chip=*/true,
Metrics::DropOutReason::GET_SCRIPTS_UNPARSABLE);
return;
}
if (response_proto.has_client_settings()) {
settings_.UpdateFromProto(response_proto.client_settings());
for (ControllerObserver& observer : observers_) {
observer.OnClientSettingsChanged(settings_);
}
}
if (response_proto.has_script_store_config()) {
GetService()->SetScriptStoreConfig(response_proto.script_store_config());
}
std::vector<std::unique_ptr<Script>> scripts;
for (const auto& script_proto : response_proto.scripts()) {
ProtocolUtils::AddScript(script_proto, &scripts);
}
autostart_timeout_script_path_ =
response_proto.script_timeout_error().script_path();
autostart_timeout_ = base::TimeDelta::FromMilliseconds(
response_proto.script_timeout_error().timeout_ms());
if (allow_autostart())
absolute_autostart_timeout_ = tick_clock_->NowTicks() + autostart_timeout_;
#ifdef NDEBUG
VLOG(2) << __func__ << " from <redacted> returned " << scripts.size()
<< " scripts";
#else
VLOG(2) << __func__ << " from " << script_url_.host() << " returned "
<< scripts.size() << " scripts";
#endif
if (VLOG_IS_ON(3)) {
for (const auto& script : scripts) {
// Strip domain from beginning if possible (redundant with log above).
auto pos = script->handle.path.find(script_url_.host());
if (pos == 0) {
DVLOG(3) << "\t"
<< script->handle.path.substr(script_url_.host().length());
} else {
DVLOG(3) << "\t" << script->handle.path;
}
}
}
if (scripts.empty()) {
script_tracker()->SetScripts({});
if (state_ == AutofillAssistantState::TRACKING) {
OnFatalError(
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR),
/*show_feedback_chip=*/false, Metrics::DropOutReason::NO_SCRIPTS);
return;
}
OnNoRunnableScriptsForPage();
}
script_tracker()->SetScripts(std::move(scripts));
GetOrCheckScripts();
}
void Controller::ExecuteScript(const std::string& script_path,
const std::string& start_message,
bool needs_ui,
std::unique_ptr<TriggerContext> context,
AutofillAssistantState end_state) {
DCHECK(!script_tracker()->running());
if (!start_message.empty())
SetStatusMessage(start_message);
EnterState(AutofillAssistantState::RUNNING);
if (needs_ui)
RequireUI();
touchable_element_area()->Clear();
// Runnable scripts will be checked and reported if necessary after executing
// the script.
script_tracker_->ClearRunnableScripts();
SetUserActions(nullptr);
// TODO(crbug.com/806868): Consider making ClearRunnableScripts part of
// ExecuteScripts to simplify the controller.
script_tracker()->ExecuteScript(
script_path, user_data_.get(), std::move(context),
base::BindOnce(&Controller::OnScriptExecuted,
// script_tracker_ is owned by Controller.
base::Unretained(this), script_path, end_state));
}
void Controller::OnScriptExecuted(const std::string& script_path,
AutofillAssistantState end_state,
const ScriptExecutor::Result& result) {
if (!result.success) {
#ifdef NDEBUG
VLOG(1) << "Failed to execute script";
#else
DVLOG(1) << "Failed to execute script " << script_path;
#endif
OnScriptError(
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR),
Metrics::DropOutReason::SCRIPT_FAILED);
return;
}
if (result.touchable_element_area) {
touchable_element_area()->SetFromProto(*result.touchable_element_area);
}
switch (result.at_end) {
case ScriptExecutor::SHUTDOWN:
if (!tracking_) {
client_->Shutdown(Metrics::DropOutReason::SCRIPT_SHUTDOWN);
return;
}
end_state = AutofillAssistantState::TRACKING;
break;
case ScriptExecutor::SHUTDOWN_GRACEFULLY:
if (!tracking_) {
EnterStoppedState(
/*show_feedback_chip=*/show_feedback_chip_on_graceful_shutdown_);
RecordDropOutOrShutdown(Metrics::DropOutReason::SCRIPT_SHUTDOWN);
return;
}
end_state = AutofillAssistantState::TRACKING;
break;
case ScriptExecutor::CLOSE_CUSTOM_TAB:
for (ControllerObserver& observer : observers_) {
observer.CloseCustomTab();
}
if (!tracking_) {
client_->Shutdown(Metrics::DropOutReason::CUSTOM_TAB_CLOSED);
return;
}
end_state = AutofillAssistantState::TRACKING;
return;
case ScriptExecutor::CONTINUE:
break;
default:
VLOG(1) << "Unexpected value for at_end: " << result.at_end;
break;
}
EnterState(end_state);
}
bool Controller::MaybeAutostartScript(
const std::vector<ScriptHandle>& runnable_scripts) {
// Under specific conditions, we can directly run a non-interrupt script
// without first displaying it. This is meant to work only at the very
// beginning, when no scripts have run, and only if there's exactly one
// autostartable script.
if (!allow_autostart())
return false;
int autostart_index = -1;
for (size_t i = 0; i < runnable_scripts.size(); i++) {
if (runnable_scripts[i].autostart) {
if (autostart_index != -1) {
// To many autostartable scripts.
return false;
}
autostart_index = i;
}
}
if (autostart_index == -1)
return false;
// Copying the strings is necessary, as ExecuteScript will invalidate
// runnable_scripts by calling ScriptTracker::ClearRunnableScripts.
//
// TODO(b/138367403): Cleanup this dangerous issue.
std::string path = runnable_scripts[autostart_index].path;
std::string start_message = runnable_scripts[autostart_index].start_message;
bool needs_ui = runnable_scripts[autostart_index].needs_ui;
ExecuteScript(path, start_message, needs_ui,
std::make_unique<TriggerContext>(),
AutofillAssistantState::PROMPT);
return true;
}
void Controller::InitFromParameters() {
auto details = std::make_unique<Details>();
if (details->UpdateFromParameters(trigger_context_->GetScriptParameters()))
SetDetails(std::move(details), base::TimeDelta());
const absl::optional<std::string> overlay_color =
trigger_context_->GetScriptParameters().GetOverlayColors();
if (overlay_color) {
std::unique_ptr<OverlayColors> colors = std::make_unique<OverlayColors>();
std::vector<std::string> color_strings =
base::SplitString(overlay_color.value(), ":", base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
if (color_strings.size() > 0) {
colors->background = color_strings[0];
}
if (color_strings.size() > 1) {
colors->highlight_border = color_strings[1];
}
// Ignore other colors, to allow future versions of the client to support
// setting more colors.
SetOverlayColors(std::move(colors));
}
const absl::optional<std::string> password_change_username =
trigger_context_->GetScriptParameters().GetPasswordChangeUsername();
if (password_change_username) {
DCHECK(GetDeeplinkURL().is_valid()); // |deeplink_url_| must be set.
user_data_->selected_login_.emplace(GetDeeplinkURL().GetOrigin(),
*password_change_username);
}
if (trigger_context_->HasExperimentId(kProgressBarExperiment)) {
ShowProgressBarProto::StepProgressBarConfiguration mock_configuration;
mock_configuration.set_use_step_progress_bar(true);
SetStepProgressBarConfiguration(mock_configuration);
}
user_model_.SetCurrentURL(GetCurrentURL());
}
void Controller::Track(std::unique_ptr<TriggerContext> trigger_context,
base::OnceCallback<void()> on_first_check_done) {
tracking_ = true;
if (state_ == AutofillAssistantState::INACTIVE) {
trigger_context_ = std::move(trigger_context);
InitFromParameters();
EnterState(AutofillAssistantState::TRACKING);
}
if (on_first_check_done) {
if (has_run_first_check_) {
std::move(on_first_check_done).Run();
} else {
on_has_run_first_check_.emplace_back(std::move(on_first_check_done));
}
}
}
bool Controller::HasRunFirstCheck() const {
return tracking_ && has_run_first_check_;
}
bool Controller::Start(const GURL& deeplink_url,
std::unique_ptr<TriggerContext> trigger_context) {
if (state_ != AutofillAssistantState::INACTIVE &&
state_ != AutofillAssistantState::TRACKING) {
return false;
}
trigger_context_ = std::move(trigger_context);
deeplink_url_ = deeplink_url;
InitFromParameters();
// Force a re-evaluation of the script, to get a chance to autostart.
if (state_ == AutofillAssistantState::TRACKING)
script_tracker_->ClearRunnableScripts();
if (IsNavigatingToNewDocument()) {
start_after_navigation_ = base::BindOnce(
&Controller::ShowFirstMessageAndStart, weak_ptr_factory_.GetWeakPtr());
} else {
ShowFirstMessageAndStart();
}
return true;
}
void Controller::ShowFirstMessageAndStart() {
// |status_message_| may be non-empty due to a trigger script that was run.
SetStatusMessage(
status_message_.empty()
? l10n_util::GetStringFUTF8(IDS_AUTOFILL_ASSISTANT_LOADING,
base::UTF8ToUTF16(GetCurrentURL().host()))
: status_message_);
if (step_progress_bar_configuration_.has_value() &&
step_progress_bar_configuration_->use_step_progress_bar()) {
if (!progress_active_step_.has_value()) {
// Set default progress unless already specified in
// |progress_active_step_|.
progress_active_step_ = 0;
}
SetStepProgressBarConfiguration(*step_progress_bar_configuration_);
SetProgressActiveStep(*progress_active_step_);
} else {
SetProgress(kAutostartInitialProgress);
}
EnterState(AutofillAssistantState::STARTING);
}
AutofillAssistantState Controller::GetState() const {
return state_;
}
bool Controller::ShouldShowOverlay() const {
return overlay_behavior_ == ConfigureUiStateProto::DEFAULT;
}
void Controller::OnScriptSelected(const ScriptHandle& handle,
std::unique_ptr<TriggerContext> context) {
ExecuteScript(handle.path, handle.start_message, handle.needs_ui,
std::move(context),
state_ == AutofillAssistantState::TRACKING
? AutofillAssistantState::TRACKING
: AutofillAssistantState::PROMPT);
}
void Controller::OnUserInteractionInsideTouchableArea() {
GetOrCheckScripts();
}
std::string Controller::GetDebugContext() {
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetKey("status", base::Value(status_message_));
if (trigger_context_) {
std::vector<base::Value> parameters_js;
for (const auto& parameter :
trigger_context_->GetScriptParameters().ToProto()) {
base::Value parameter_js = base::Value(base::Value::Type::DICTIONARY);
parameter_js.SetKey(parameter.name(), base::Value(parameter.value()));
parameters_js.push_back(std::move(parameter_js));
}
dict.SetKey("parameters", base::Value(parameters_js));
}
dict.SetKey("scripts", script_tracker()->GetDebugContext());
std::vector<base::Value> details_list;
for (const auto& holder : details_) {
details_list.push_back(holder.GetDetails().GetDebugContext());
}
dict.SetKey("details", base::Value(details_list));
std::string output_js;
base::JSONWriter::Write(dict, &output_js);
return output_js;
}
const CollectUserDataOptions* Controller::GetCollectUserDataOptions() const {
return collect_user_data_options_;
}
const UserData* Controller::GetUserData() const {
return user_data_.get();
}
void Controller::OnCollectUserDataContinueButtonClicked() {
if (!collect_user_data_options_ || !user_data_)
return;
auto callback = std::move(collect_user_data_options_->confirm_callback);
SetCollectUserDataOptions(nullptr);
std::move(callback).Run(user_data_.get(), &user_model_);
}
void Controller::OnCollectUserDataAdditionalActionTriggered(int index) {
if (!collect_user_data_options_)
return;
auto callback =
std::move(collect_user_data_options_->additional_actions_callback);
SetCollectUserDataOptions(nullptr);
std::move(callback).Run(index, user_data_.get(), &user_model_);
}
void Controller::OnTextLinkClicked(int link) {
if (!user_data_)
return;
auto callback = std::move(collect_user_data_options_->terms_link_callback);
SetCollectUserDataOptions(nullptr);
std::move(callback).Run(link, user_data_.get(), &user_model_);
}
void Controller::OnFormActionLinkClicked(int link) {
if (form_cancel_callback_ && form_result_ != nullptr) {
form_result_->set_link(link);
form_changed_callback_.Run(form_result_.get());
std::move(form_cancel_callback_).Run(ClientStatus(ACTION_APPLIED));
}
}
void Controller::SetDateTimeRangeStartDate(
const absl::optional<DateProto>& date) {
if (!user_data_)
return;
if (user_data_->date_time_range_start_date_.has_value() && date.has_value() &&
CollectUserDataAction::CompareDates(
*user_data_->date_time_range_start_date_, *date) == 0) {
return;
}
user_data_->date_time_range_start_date_ = date;
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_START);
}
if (CollectUserDataAction::SanitizeDateTimeRange(
&user_data_->date_time_range_start_date_,
&user_data_->date_time_range_start_timeslot_,
&user_data_->date_time_range_end_date_,
&user_data_->date_time_range_end_timeslot_,
*collect_user_data_options_,
/* change_start = */ false)) {
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_END);
}
}
UpdateCollectUserDataActions();
}
void Controller::SetDateTimeRangeStartTimeSlot(
const absl::optional<int>& timeslot_index) {
if (!user_data_)
return;
if (user_data_->date_time_range_start_timeslot_.has_value() &&
timeslot_index.has_value() &&
*user_data_->date_time_range_start_timeslot_ == *timeslot_index) {
return;
}
user_data_->date_time_range_start_timeslot_ = timeslot_index;
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_START);
}
if (CollectUserDataAction::SanitizeDateTimeRange(
&user_data_->date_time_range_start_date_,
&user_data_->date_time_range_start_timeslot_,
&user_data_->date_time_range_end_date_,
&user_data_->date_time_range_end_timeslot_,
*collect_user_data_options_,
/* change_start = */ false)) {
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_END);
}
}
UpdateCollectUserDataActions();
}
void Controller::SetDateTimeRangeEndDate(
const absl::optional<DateProto>& date) {
if (!user_data_)
return;
if (user_data_->date_time_range_end_date_.has_value() && date.has_value() &&
CollectUserDataAction::CompareDates(
*user_data_->date_time_range_end_date_, *date) == 0) {
return;
}
user_data_->date_time_range_end_date_ = date;
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_END);
}
if (CollectUserDataAction::SanitizeDateTimeRange(
&user_data_->date_time_range_start_date_,
&user_data_->date_time_range_start_timeslot_,
&user_data_->date_time_range_end_date_,
&user_data_->date_time_range_end_timeslot_,
*collect_user_data_options_,
/* change_start = */ true)) {
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_START);
}
}
UpdateCollectUserDataActions();
}
void Controller::SetDateTimeRangeEndTimeSlot(
const absl::optional<int>& timeslot_index) {
if (!user_data_)
return;
if (user_data_->date_time_range_end_timeslot_.has_value() &&
timeslot_index.has_value() &&
*user_data_->date_time_range_end_timeslot_ == *timeslot_index) {
return;
}
user_data_->date_time_range_end_timeslot_ = timeslot_index;
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_END);
}
if (CollectUserDataAction::SanitizeDateTimeRange(
&user_data_->date_time_range_start_date_,
&user_data_->date_time_range_start_timeslot_,
&user_data_->date_time_range_end_date_,
&user_data_->date_time_range_end_timeslot_,
*collect_user_data_options_,
/* change_start = */ true)) {
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::DATE_TIME_RANGE_START);
}
}
UpdateCollectUserDataActions();
}
void Controller::SetAdditionalValue(const std::string& client_memory_key,
const ValueProto& value) {
if (!user_data_)
return;
auto it = user_data_->additional_values_.find(client_memory_key);
if (it == user_data_->additional_values_.end()) {
NOTREACHED() << client_memory_key << " not found";
return;
}
it->second = value;
UpdateCollectUserDataActions();
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::ADDITIONAL_VALUES);
}
}
void Controller::SetShippingAddress(
std::unique_ptr<autofill::AutofillProfile> address) {
if (collect_user_data_options_ == nullptr) {
return;
}
DCHECK(!collect_user_data_options_->shipping_address_name.empty());
SetProfile(collect_user_data_options_->shipping_address_name,
UserData::FieldChange::SHIPPING_ADDRESS, std::move(address));
}
void Controller::SetContactInfo(
std::unique_ptr<autofill::AutofillProfile> profile) {
if (collect_user_data_options_ == nullptr) {
return;
}
DCHECK(!collect_user_data_options_->contact_details_name.empty());
SetProfile(collect_user_data_options_->contact_details_name,
UserData::FieldChange::CONTACT_PROFILE, std::move(profile));
}
void Controller::SetCreditCard(
std::unique_ptr<autofill::CreditCard> card,
std::unique_ptr<autofill::AutofillProfile> billing_profile) {
if (user_data_ == nullptr || collect_user_data_options_ == nullptr) {
return;
}
DCHECK(!collect_user_data_options_->billing_address_name.empty());
user_model_.SetSelectedCreditCard(std::move(card), user_data_.get());
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(), UserData::FieldChange::CARD);
}
SetProfile(collect_user_data_options_->billing_address_name,
UserData::FieldChange::BILLING_ADDRESS,
std::move(billing_profile));
}
void Controller::SetProfile(
const std::string& key,
UserData::FieldChange field_change,
std::unique_ptr<autofill::AutofillProfile> profile) {
if (user_data_ == nullptr) {
return;
}
user_model_.SetSelectedAutofillProfile(key, std::move(profile),
user_data_.get());
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(), field_change);
}
UpdateCollectUserDataActions();
}
void Controller::SetTermsAndConditions(
TermsAndConditionsState terms_and_conditions) {
if (!user_data_)
return;
user_data_->terms_and_conditions_ = terms_and_conditions;
UpdateCollectUserDataActions();
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::TERMS_AND_CONDITIONS);
}
}
void Controller::SetLoginOption(std::string identifier) {
if (!user_data_ || !collect_user_data_options_)
return;
user_data_->login_choice_identifier_.assign(identifier);
UpdateCollectUserDataActions();
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(),
UserData::FieldChange::LOGIN_CHOICE);
}
}
void Controller::UpdateCollectUserDataActions() {
// TODO(crbug.com/806868): This method uses #SetUserActions(), which means
// that updating the PR action buttons will also clear the suggestions. We
// should update the action buttons only if there are use cases of PR +
// suggestions.
if (!collect_user_data_options_ || !user_data_) {
SetUserActions(nullptr);
return;
}
bool confirm_button_enabled = CollectUserDataAction::IsUserDataComplete(
*user_data_, user_model_, *collect_user_data_options_);
UserAction confirm(collect_user_data_options_->confirm_action);
confirm.SetEnabled(confirm_button_enabled);
if (confirm_button_enabled) {
confirm.SetCallback(
base::BindOnce(&Controller::OnCollectUserDataContinueButtonClicked,
weak_ptr_factory_.GetWeakPtr()));
}
auto user_actions = std::make_unique<std::vector<UserAction>>();
user_actions->emplace_back(std::move(confirm));
// Add additional actions.
for (size_t i = 0; i < collect_user_data_options_->additional_actions.size();
++i) {
auto action = collect_user_data_options_->additional_actions[i];
user_actions->push_back({action});
user_actions->back().SetCallback(
base::BindOnce(&Controller::OnCollectUserDataAdditionalActionTriggered,
weak_ptr_factory_.GetWeakPtr(), i));
}
SetUserActions(std::move(user_actions));
}
void Controller::GetTouchableArea(std::vector<RectF>* area) const {
if (touchable_element_area_)
touchable_element_area_->GetTouchableRectangles(area);
}
void Controller::GetRestrictedArea(std::vector<RectF>* area) const {
if (touchable_element_area_)
touchable_element_area_->GetRestrictedRectangles(area);
}
void Controller::GetVisualViewport(RectF* visual_viewport) const {
if (touchable_element_area_)
touchable_element_area_->GetVisualViewport(visual_viewport);
}
void Controller::OnScriptError(const std::string& error_message,
Metrics::DropOutReason reason) {
if (state_ == AutofillAssistantState::STOPPED)
return;
RequireUI();
SetStatusMessage(error_message);
SetProgressBarErrorState(true);
EnterStoppedState(/*show_feedback_chip=*/true);
if (tracking_) {
EnterState(AutofillAssistantState::TRACKING);
return;
}
RecordDropOutOrShutdown(reason);
}
void Controller::OnFatalError(const std::string& error_message,
bool show_feedback_chip,
Metrics::DropOutReason reason) {
LOG(ERROR) << "Autofill Assistant has encountered a fatal error and is "
"shutting down, reason="
<< reason;
if (state_ == AutofillAssistantState::STOPPED)
return;
SetStatusMessage(error_message);
SetProgressBarErrorState(true);
EnterStoppedState(show_feedback_chip);
// If we haven't managed to check the set of scripts yet at this point, we
// never will.
MaybeReportFirstCheckDone();
if (tracking_ && script_url_.host() == GetCurrentURL().host()) {
// When tracking the controller should stays until the browser has navigated
// away from the last domain that was checked to be able to tell callers
// that the set of user actions is empty.
delayed_shutdown_reason_ = reason;
return;
}
RecordDropOutOrShutdown(reason);
}
void Controller::RecordDropOutOrShutdown(Metrics::DropOutReason reason) {
// If there is an UI, we wait for it to be closed before shutting down (the UI
// will call |ShutdownIfNecessary|).
if (client_->HasHadUI()) {
// We report right away to make sure we don't lose this reason if the client
// is unexpectedly destroyed while the error message is showing (for example
// if the tab is closed).
client_->RecordDropOut(reason);
} else {
client_->Shutdown(reason);
}
}
void Controller::OnStop(const std::string& message,
const std::string& button_label) {
DCHECK(state_ != AutofillAssistantState::STOPPED);
can_recover_from_stopped_ = true;
for (auto& listener : listeners_) {
listener.OnPause(message, button_label);
}
}
void Controller::PerformDelayedShutdownIfNecessary() {
if (delayed_shutdown_reason_ &&
script_url_.host() != GetCurrentURL().host()) {
Metrics::DropOutReason reason = delayed_shutdown_reason_.value();
delayed_shutdown_reason_ = absl::nullopt;
tracking_ = false;
client_->Shutdown(reason);
}
}
void Controller::MaybeReportFirstCheckDone() {
if (has_run_first_check_)
return;
has_run_first_check_ = true;
while (!on_has_run_first_check_.empty()) {
std::move(on_has_run_first_check_.back()).Run();
on_has_run_first_check_.pop_back();
}
}
void Controller::OnNoRunnableScriptsForPage() {
if (script_tracker()->running())
return;
switch (state_) {
case AutofillAssistantState::STARTING:
// We're still waiting for the set of initial scripts, but either didn't
// get any scripts or didn't get scripts that could possibly become
// runnable with a DOM change.
OnScriptError(
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR),
Metrics::DropOutReason::NO_INITIAL_SCRIPTS);
break;
case AutofillAssistantState::PROMPT:
case AutofillAssistantState::AUTOSTART_FALLBACK_PROMPT:
// The user has navigated to a page that has no scripts or the scripts
// have reached a state from which they cannot recover through a DOM
// change.
OnScriptError(l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_GIVE_UP),
Metrics::DropOutReason::NO_SCRIPTS);
break;
default:
// Always having a set of scripts to potentially run is not required in
// other states, for example in BROWSE state.
break;
}
}
void Controller::OnRunnableScriptsChanged(
const std::vector<ScriptHandle>& runnable_scripts) {
base::ScopedClosureRunner report_first_check;
if (!has_run_first_check_) {
// Only report first check done once we're done processing the given set of
// scripts - whatever the outcome - so callers can see that outcome in the
// state of the controller.
report_first_check.ReplaceClosure(
base::BindOnce(&Controller::MaybeReportFirstCheckDone,
weak_ptr_factory_.GetWeakPtr()));
}
// Script selection is disabled when a script is already running. We will
// check again and maybe update when the current script has finished.
if (script_tracker()->running() || state_ == AutofillAssistantState::STOPPED)
return;
if (MaybeAutostartScript(runnable_scripts)) {
return;
}
// Show the initial prompt if available.
for (const auto& script : runnable_scripts) {
// runnable_scripts is ordered by priority.
if (!script.initial_prompt.empty()) {
SetStatusMessage(script.initial_prompt);
break;
}
}
// Update the set of user actions to report.
auto user_actions = std::make_unique<std::vector<UserAction>>();
for (const auto& script : runnable_scripts) {
UserAction user_action;
user_action.chip() = script.chip;
user_action.direct_action() = script.direct_action;
if (!user_action.has_triggers())
continue;
user_action.SetCallback(base::BindOnce(
&Controller::OnScriptSelected, weak_ptr_factory_.GetWeakPtr(), script));
user_actions->emplace_back(std::move(user_action));
}
// Change state, if necessary.
switch (state_) {
case AutofillAssistantState::TRACKING:
case AutofillAssistantState::AUTOSTART_FALLBACK_PROMPT:
case AutofillAssistantState::PROMPT:
case AutofillAssistantState::BROWSE:
// Don't change state
break;
case AutofillAssistantState::STARTING:
if (!user_actions->empty())
EnterState(AutofillAssistantState::AUTOSTART_FALLBACK_PROMPT);
break;
default:
if (!user_actions->empty())
EnterState(AutofillAssistantState::PROMPT);
}
SetUserActions(std::move(user_actions));
}
void Controller::DidFinishLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
// validated_url might not be the page URL. Ignore it and always check the
// last committed url.
OnUrlChange();
}
void Controller::ExpectNavigation() {
expect_navigation_ = true;
}
void Controller::OnNavigationShutdownOrError(const GURL& url,
Metrics::DropOutReason reason) {
if (google_util::IsGoogleDomainUrl(
url, google_util::ALLOW_SUBDOMAIN,
google_util::DISALLOW_NON_STANDARD_PORTS)) {
client_->Shutdown(reason);
} else {
OnScriptError(l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_GIVE_UP),
reason);
}
}
void Controller::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
if (!navigating_to_new_document_) {
navigating_to_new_document_ = true;
ReportNavigationStateChanged();
}
// The navigation is expected, do not check for errors below.
if (expect_navigation_) {
expect_navigation_ = false;
return;
}
if (state_ == AutofillAssistantState::STOPPED &&
!navigation_handle->IsRendererInitiated() &&
!navigation_handle->WasServerRedirect()) {
if (can_recover_from_stopped_) {
// Usually when in STOPPED (e.g. through |OnScriptError|) the
// |DropOutReason| has been recorded. In the case of a recoverable stop,
// e.g. with the back button, this is not the case. Record the reason as
// |NAVIGATION| here.
client_->Shutdown(Metrics::DropOutReason::NAVIGATION);
return;
}
ShutdownIfNecessary();
return;
}
// In regular scripts, the following types of navigations are allowed for the
// main frame, when in PROMPT state:
// - first-time URL load
// - script-directed navigation, while a script is running unless
// there's a touchable area.
// - server redirections, which might happen outside of a script, but
// because of a load triggered by a previously-running script.
// - same-document modifications, which might happen automatically
// - javascript-initiated navigation or refresh
// - navigation by clicking on a link
// In the last two cases, autofill assistant might still give up later on if
// it discovers that the new page has no scripts.
//
// Everything else, such as going back to a previous page, or refreshing the
// page is considered an end condition. If going back to a previous page is
// required, consider using the BROWSE state instead.
if (state_ == AutofillAssistantState::PROMPT &&
web_contents()->GetLastCommittedURL().is_valid() &&
!navigation_handle->WasServerRedirect() &&
!navigation_handle->IsRendererInitiated()) {
OnNavigationShutdownOrError(navigation_handle->GetURL(),
Metrics::DropOutReason::NAVIGATION);
return;
}
// When in RUNNING state, all renderer initiated navigation is allowed,
// user initiated navigation will cause an error.
if (state_ == AutofillAssistantState::RUNNING &&
!navigation_handle->WasServerRedirect() &&
!navigation_handle->IsRendererInitiated()) {
OnNavigationShutdownOrError(
navigation_handle->GetURL(),
Metrics::DropOutReason::NAVIGATION_WHILE_RUNNING);
return;
}
// Note that BROWSE state end conditions are in DidFinishNavigation, in order
// to be able to properly evaluate the committed url.
}
void Controller::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
// TODO(b/159871774): Rethink how we handle navigation events. The early
// return here may prevent us from updating |navigating_to_new_document_|.
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument() ||
!navigation_handle->HasCommitted() || !IsNavigatingToNewDocument()) {
return;
}
bool is_successful =
!navigation_handle->IsErrorPage() &&
navigation_handle->GetNetErrorCode() == net::OK &&
navigation_handle->GetResponseHeaders() &&
(navigation_handle->GetResponseHeaders()->response_code() / 100) == 2;
navigation_error_ = !is_successful;
navigating_to_new_document_ = false;
// When in BROWSE state, stop autofill assistant if the user navigates away
// from the original assisted domain. Subdomains of the original domain are
// supported. If the new URL is on a Google property, destroy the UI
// immediately, without showing an error.
if (state_ == AutofillAssistantState::BROWSE) {
if (!url_utils::IsInDomainOrSubDomain(GetCurrentURL(), script_url_) &&
!url_utils::IsInDomainOrSubDomain(GetCurrentURL(),
browse_domains_allowlist_)) {
OnNavigationShutdownOrError(
web_contents()->GetLastCommittedURL(),
Metrics::DropOutReason::DOMAIN_CHANGE_DURING_BROWSE_MODE);
}
}
if (start_after_navigation_) {
std::move(start_after_navigation_).Run();
} else {
ReportNavigationStateChanged();
if (is_successful) {
OnUrlChange();
}
}
}
void Controller::DocumentAvailableInMainFrame(
content::RenderFrameHost* render_frame_host) {
OnUrlChange();
}
void Controller::RenderProcessGone(base::TerminationStatus status) {
client_->Shutdown(Metrics::DropOutReason::RENDER_PROCESS_GONE);
}
void Controller::OnWebContentsFocused(
content::RenderWidgetHost* render_widget_host) {
if (NeedsUI() &&
base::FeatureList::IsEnabled(features::kAutofillAssistantChromeEntry)) {
// Show UI again when re-focused in case the web contents moved activity.
// This is only enabled when tab-switching is enabled.
client_->AttachUI();
}
}
void Controller::OnValueChanged(const std::string& identifier,
const ValueProto& new_value) {
event_handler_.DispatchEvent({EventProto::kOnValueChanged, identifier});
// TODO(b/145043394) Remove this once chips are part of generic UI.
if (collect_user_data_options_ != nullptr &&
collect_user_data_options_->additional_model_identifier_to_check
.has_value() &&
identifier ==
*collect_user_data_options_->additional_model_identifier_to_check) {
UpdateCollectUserDataActions();
}
}
void Controller::OnTouchableAreaChanged(
const RectF& visual_viewport,
const std::vector<RectF>& touchable_areas,
const std::vector<RectF>& restricted_areas) {
for (ControllerObserver& observer : observers_) {
observer.OnTouchableAreaChanged(visual_viewport, touchable_areas,
restricted_areas);
}
}
void Controller::SetCollectUserDataOptions(CollectUserDataOptions* options) {
DCHECK(!options ||
(options->confirm_callback && options->additional_actions_callback &&
options->terms_link_callback));
if (collect_user_data_options_ == nullptr && options == nullptr)
return;
collect_user_data_options_ = options;
UpdateCollectUserDataActions();
for (ControllerObserver& observer : observers_) {
observer.OnCollectUserDataOptionsChanged(collect_user_data_options_);
observer.OnUserDataChanged(user_data_.get(), UserData::FieldChange::ALL);
}
}
void Controller::SetLastSuccessfulUserDataOptions(
std::unique_ptr<CollectUserDataOptions> collect_user_data_options) {
last_collect_user_data_options_ = std::move(collect_user_data_options);
}
const CollectUserDataOptions* Controller::GetLastSuccessfulUserDataOptions()
const {
return last_collect_user_data_options_.get();
}
void Controller::WriteUserData(
base::OnceCallback<void(UserData*, UserData::FieldChange*)>
write_callback) {
UserData::FieldChange field_change = UserData::FieldChange::NONE;
std::move(write_callback).Run(user_data_.get(), &field_change);
if (field_change == UserData::FieldChange::NONE) {
return;
}
for (ControllerObserver& observer : observers_) {
observer.OnUserDataChanged(user_data_.get(), field_change);
}
UpdateCollectUserDataActions();
}
bool Controller::StateNeedsUI(AutofillAssistantState state) {
// Note that the UI might be shown in RUNNING state, even if it doesn't
// require it.
switch (state) {
case AutofillAssistantState::PROMPT:
case AutofillAssistantState::AUTOSTART_FALLBACK_PROMPT:
case AutofillAssistantState::MODAL_DIALOG:
case AutofillAssistantState::STARTING:
return true;
case AutofillAssistantState::INACTIVE:
case AutofillAssistantState::TRACKING:
case AutofillAssistantState::STOPPED:
case AutofillAssistantState::RUNNING:
return false;
case AutofillAssistantState::BROWSE:
return browse_mode_invisible_;
}
}
void Controller::OnKeyboardVisibilityChanged(bool visible) {
is_keyboard_showing_ = visible;
SetVisibilityAndUpdateUserActions();
}
ElementArea* Controller::touchable_element_area() {
if (!touchable_element_area_) {
touchable_element_area_ = std::make_unique<ElementArea>(this);
touchable_element_area_->SetOnUpdate(base::BindRepeating(
&Controller::OnTouchableAreaChanged, weak_ptr_factory_.GetWeakPtr()));
}
return touchable_element_area_.get();
}
ScriptTracker* Controller::script_tracker() {
if (!script_tracker_) {
script_tracker_ = std::make_unique<ScriptTracker>(/* delegate= */ this,
/* listener= */ this);
}
return script_tracker_.get();
}
} // namespace autofill_assistant