| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/quick_answers/quick_answers_ui_controller.h" |
| |
| #include <optional> |
| |
| #include "base/check_is_test.h" |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/strings/stringprintf.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/ui/chromeos/read_write_cards/read_write_cards_ui_controller.h" |
| #include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h" |
| #include "chrome/browser/ui/quick_answers/ui/quick_answers_util.h" |
| #include "chrome/browser/ui/quick_answers/ui/quick_answers_view.h" |
| #include "chrome/browser/ui/quick_answers/ui/rich_answers_definition_view.h" |
| #include "chrome/browser/ui/quick_answers/ui/rich_answers_translation_view.h" |
| #include "chrome/browser/ui/quick_answers/ui/rich_answers_unit_conversion_view.h" |
| #include "chrome/browser/ui/quick_answers/ui/rich_answers_view.h" |
| #include "chrome/browser/ui/quick_answers/ui/user_consent_view.h" |
| #include "chromeos/components/quick_answers/public/cpp/constants.h" |
| #include "chromeos/components/quick_answers/public/cpp/controller/quick_answers_controller.h" |
| #include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h" |
| #include "chromeos/components/quick_answers/quick_answers_model.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/strings/grit/chromeos_strings.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/views/metadata/view_factory_internal.h" |
| #include "ui/views/widget/widget.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "ash/public/cpp/new_window_delegate.h" |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/browser_commands.h" // nogncheck |
| #include "chrome/browser/ui/browser_finder.h" // nogncheck |
| #include "chrome/browser/ui/browser_navigator.h" // nogncheck |
| #include "chrome/browser/ui/browser_navigator_params.h" // nogncheck |
| #include "chromeos/crosapi/mojom/url_handler.mojom.h" |
| #include "chromeos/lacros/lacros_service.h" |
| #endif // BUILDFLAG(IS_CHROMEOS_LACROS) |
| |
| namespace { |
| |
| using quick_answers::QuickAnswer; |
| using quick_answers::QuickAnswersExitPoint; |
| |
| constexpr char kFeedbackDescriptionTemplate[] = "#QuickAnswers\nQuery:%s\n"; |
| |
| constexpr char kQuickAnswersSettingsUrl[] = |
| "chrome://os-settings/osSearch/search"; |
| |
| // Open the specified URL in a new tab with the specified profile |
| void OpenUrl(Profile* profile, const GURL& url) { |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| // We always want to open a link in Lacros browser if LacrosOnly is true. |
| // `GetPrimary` returns a proper delegate depending on the flag. |
| ash::NewWindowDelegate::GetPrimary()->OpenUrl( |
| url, ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction, |
| ash::NewWindowDelegate::Disposition::kNewForegroundTab); |
| #elif BUILDFLAG(IS_CHROMEOS_LACROS) |
| NavigateParams navigate_params( |
| profile, url, |
| ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_FROM_API)); |
| navigate_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| navigate_params.window_action = NavigateParams::SHOW_WINDOW; |
| Navigate(&navigate_params); |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| } |
| |
| quick_answers::Design GetDesign(QuickAnswersState::FeatureType feature_type) { |
| switch (feature_type) { |
| case QuickAnswersState::FeatureType::kQuickAnswers: |
| return chromeos::features::IsQuickAnswersMaterialNextUIEnabled() |
| ? quick_answers::Design::kRefresh |
| : quick_answers::Design::kCurrent; |
| case QuickAnswersState::FeatureType::kHmr: |
| return quick_answers::Design::kMagicBoost; |
| } |
| |
| CHECK(false) << "Invalid feature type enum value provided"; |
| } |
| |
| } // namespace |
| |
| using chromeos::ReadWriteCardsUiController; |
| |
| QuickAnswersUiController::QuickAnswersUiController( |
| QuickAnswersControllerImpl* controller) |
| : controller_(controller) {} |
| |
| QuickAnswersUiController::~QuickAnswersUiController() { |
| // Created Quick Answers UIs (e.g., `UserConsentView`) can have dependency to |
| // `QuickAnswersUiController`. Destruct those UIs before destructing a UI |
| // controller. Note that `RemoveQuickAnswersUi` is no-op if no Quick Answers |
| // UI is currently shown. |
| GetReadWriteCardsUiController().RemoveQuickAnswersUi(); |
| } |
| |
| void QuickAnswersUiController::CreateQuickAnswersView( |
| Profile* profile, |
| const std::string& title, |
| const std::string& query, |
| std::optional<quick_answers::Intent> intent, |
| QuickAnswersState::FeatureType feature_type, |
| bool is_internal) { |
| CreateQuickAnswersViewInternal(profile, query, intent, |
| { |
| .title = title, |
| .design = GetDesign(feature_type), |
| .is_internal = is_internal, |
| }); |
| } |
| |
| void QuickAnswersUiController::CreateQuickAnswersViewForPixelTest( |
| Profile* profile, |
| const std::string& query, |
| std::optional<quick_answers::Intent> intent, |
| quick_answers::QuickAnswersView::Params params) { |
| CHECK_IS_TEST(); |
| CreateQuickAnswersViewInternal(profile, query, intent, params); |
| } |
| |
| void QuickAnswersUiController::CreateQuickAnswersViewInternal( |
| Profile* profile, |
| const std::string& query, |
| std::optional<quick_answers::Intent> intent, |
| quick_answers::QuickAnswersView::Params params) { |
| // Currently there are timing issues that causes the quick answers view is not |
| // dismissed. TODO(updowndota): Remove the special handling after the root |
| // cause is found. |
| if (IsShowingQuickAnswersView()) { |
| LOG(ERROR) << "Quick answers view not dismissed."; |
| CloseQuickAnswersView(); |
| } |
| |
| DCHECK(!IsShowingUserConsentView()); |
| SetActiveQuery(profile, query); |
| |
| auto* view = GetReadWriteCardsUiController().SetQuickAnswersUi( |
| views::Builder<quick_answers::QuickAnswersView>( |
| std::make_unique<quick_answers::QuickAnswersView>( |
| params, |
| /*controller=*/weak_factory_.GetWeakPtr())) |
| .CustomConfigure(base::BindOnce( |
| [](std::optional<quick_answers::Intent> intent, |
| quick_answers::QuickAnswersView* quick_answers_view) { |
| if (intent) { |
| quick_answers_view->SetIntent(intent.value()); |
| } |
| }, |
| intent)) |
| .Build()); |
| |
| quick_answers_view_.SetView(view); |
| } |
| |
| void QuickAnswersUiController::CreateRichAnswersView() { |
| CHECK(controller_->quick_answer()); |
| |
| views::UniqueWidgetPtr widget = quick_answers::RichAnswersView::CreateWidget( |
| controller_->anchor_bounds(), weak_factory_.GetWeakPtr(), |
| *controller_->quick_answer(), *controller_->structured_result()); |
| |
| if (!widget) { |
| // If the rich card widget cannot be created, fall-back to open the query |
| // in Google Search. |
| OpenUrl(profile_, quick_answers::GetDetailsUrlForQuery(query_)); |
| controller_->OnQuickAnswersResultClick(); |
| } |
| |
| rich_answers_widget_ = std::move(widget); |
| rich_answers_widget_->Show(); |
| controller_->SetVisibility(QuickAnswersVisibility::kRichAnswersVisible); |
| return; |
| } |
| |
| void QuickAnswersUiController::OnQuickAnswersViewPressed() { |
| // Route dismissal through |controller_| for logging impressions. |
| controller_->DismissQuickAnswers(QuickAnswersExitPoint::kQuickAnswersClick); |
| |
| // Trigger the corresponding rich card view if the feature is enabled. |
| if (chromeos::features::IsQuickAnswersRichCardEnabled() && |
| controller_->quick_answer() != nullptr) { |
| CreateRichAnswersView(); |
| return; |
| } |
| |
| OpenWebUrl(quick_answers::GetDetailsUrlForQuery(query_)); |
| |
| if (controller_->quick_answers_session()) { |
| controller_->OnQuickAnswersResultClick(); |
| } |
| } |
| |
| void QuickAnswersUiController::OnGoogleSearchLabelPressed() { |
| OpenWebUrl(quick_answers::GetDetailsUrlForQuery(query_)); |
| |
| // Route dismissal through |controller_| for logging impressions. |
| controller_->DismissQuickAnswers(QuickAnswersExitPoint::kUnspecified); |
| } |
| |
| bool QuickAnswersUiController::CloseQuickAnswersView() { |
| if (controller_->GetQuickAnswersVisibility() == |
| QuickAnswersVisibility::kQuickAnswersVisible) { |
| GetReadWriteCardsUiController().RemoveQuickAnswersUi(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool QuickAnswersUiController::CloseRichAnswersView() { |
| if (IsShowingRichAnswersView()) { |
| rich_answers_widget_->Close(); |
| return true; |
| } |
| return false; |
| } |
| |
| void QuickAnswersUiController::OnRetryLabelPressed() { |
| if (!fake_on_retry_label_pressed_callback_.is_null()) { |
| CHECK_IS_TEST(); |
| fake_on_retry_label_pressed_callback_.Run(); |
| return; |
| } |
| |
| controller_->OnRetryQuickAnswersRequest(); |
| } |
| |
| void QuickAnswersUiController::SetFakeOnRetryLabelPressedCallbackForTesting( |
| QuickAnswersUiController::FakeOnRetryLabelPressedCallback |
| fake_on_retry_label_pressed_callback) { |
| CHECK_IS_TEST(); |
| CHECK(!fake_on_retry_label_pressed_callback.is_null()); |
| CHECK(fake_on_retry_label_pressed_callback_.is_null()); |
| fake_on_retry_label_pressed_callback_ = fake_on_retry_label_pressed_callback; |
| } |
| |
| void QuickAnswersUiController::RenderQuickAnswersViewWithResult( |
| const quick_answers::StructuredResult& structured_result) { |
| if (!IsShowingQuickAnswersView()) |
| return; |
| |
| // QuickAnswersView was initiated with a loading page and will be updated |
| // when quick answers result from server side is ready. |
| quick_answers_view()->SetResult(structured_result); |
| } |
| |
| void QuickAnswersUiController::SetActiveQuery(Profile* profile, |
| const std::string& query) { |
| profile_ = profile; |
| query_ = query; |
| } |
| |
| void QuickAnswersUiController::ShowRetry() { |
| if (!IsShowingQuickAnswersView()) |
| return; |
| |
| quick_answers_view()->ShowRetryView(); |
| } |
| |
| void QuickAnswersUiController::CreateUserConsentView( |
| const gfx::Rect& anchor_bounds, |
| quick_answers::IntentType intent_type, |
| const std::u16string& intent_text) { |
| CreateUserConsentViewInternal( |
| anchor_bounds, intent_type, intent_text, |
| /*use_refreshed_design=*/ |
| chromeos::features::IsQuickAnswersMaterialNextUIEnabled()); |
| } |
| |
| void QuickAnswersUiController::CreateUserConsentViewForPixelTest( |
| const gfx::Rect& anchor_bounds, |
| quick_answers::IntentType intent_type, |
| const std::u16string& intent_text, |
| bool use_refreshed_design) { |
| CHECK_IS_TEST(); |
| CreateUserConsentViewInternal(anchor_bounds, intent_type, intent_text, |
| use_refreshed_design); |
| } |
| |
| void QuickAnswersUiController::CreateUserConsentViewInternal( |
| const gfx::Rect& anchor_bounds, |
| quick_answers::IntentType intent_type, |
| const std::u16string& intent_text, |
| bool use_refreshed_design) { |
| CHECK_EQ(controller_->GetQuickAnswersVisibility(), |
| QuickAnswersVisibility::kPending); |
| |
| auto* view = GetReadWriteCardsUiController().SetQuickAnswersUi( |
| views::Builder<quick_answers::UserConsentView>( |
| std::make_unique<quick_answers::UserConsentView>( |
| use_refreshed_design, GetReadWriteCardsUiController())) |
| .SetIntentType(intent_type) |
| .SetIntentText(intent_text) |
| // It is safe to do `base::Unretained(this)`. UIs are destructed |
| // before a UI controller gets destructed. See |
| // `~QuickAnswersUiController`. |
| .SetNoThanksButtonPressed(base::BindRepeating( |
| &QuickAnswersUiController::OnUserConsentNoThanksPressed, |
| base::Unretained(this))) |
| .SetAllowButtonPressed(base::BindRepeating( |
| &QuickAnswersUiController::OnUserConsentAllowPressed, |
| base::Unretained(this))) |
| .Build()); |
| user_consent_view_.SetView(view); |
| } |
| |
| void QuickAnswersUiController::CloseUserConsentView() { |
| CHECK_EQ(controller_->GetQuickAnswersVisibility(), |
| QuickAnswersVisibility::kUserConsentVisible); |
| GetReadWriteCardsUiController().RemoveQuickAnswersUi(); |
| } |
| |
| void QuickAnswersUiController::OnSettingsButtonPressed() { |
| // Route dismissal through |controller_| for logging impressions. |
| controller_->DismissQuickAnswers(QuickAnswersExitPoint::kSettingsButtonClick); |
| |
| OpenSettings(); |
| } |
| |
| void QuickAnswersUiController::OpenSettings() { |
| if (!fake_open_settings_callback_.is_null()) { |
| CHECK_IS_TEST(); |
| fake_open_settings_callback_.Run(); |
| return; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| OpenUrl(profile_, GURL(kQuickAnswersSettingsUrl)); |
| #elif BUILDFLAG(IS_CHROMEOS_LACROS) |
| // OS settings app is implemented in Ash, but OpenUrl here does not qualify |
| // for redirection in Lacros due to security limitations. Thus we need to |
| // explicitly send the request to Ash to launch the OS settings app. |
| chromeos::LacrosService* service = chromeos::LacrosService::Get(); |
| DCHECK(service->IsAvailable<crosapi::mojom::UrlHandler>()); |
| |
| service->GetRemote<crosapi::mojom::UrlHandler>()->OpenUrl( |
| GURL(kQuickAnswersSettingsUrl)); |
| #endif |
| } |
| |
| void QuickAnswersUiController::SetFakeOpenSettingsCallbackForTesting( |
| QuickAnswersUiController::FakeOpenSettingsCallback |
| fake_open_settings_callback) { |
| CHECK_IS_TEST(); |
| CHECK(!fake_open_settings_callback.is_null()); |
| CHECK(fake_open_settings_callback_.is_null()); |
| fake_open_settings_callback_ = fake_open_settings_callback; |
| } |
| |
| void QuickAnswersUiController::OnReportQueryButtonPressed() { |
| controller_->DismissQuickAnswers( |
| QuickAnswersExitPoint::kReportQueryButtonClick); |
| |
| OpenFeedbackPage( |
| base::StringPrintf(kFeedbackDescriptionTemplate, query_.c_str())); |
| } |
| |
| void QuickAnswersUiController::SetFakeOpenFeedbackPageCallbackForTesting( |
| QuickAnswersUiController::FakeOpenFeedbackPageCallback |
| fake_open_feedback_page_callback) { |
| CHECK_IS_TEST(); |
| CHECK(!fake_open_feedback_page_callback.is_null()); |
| CHECK(fake_open_feedback_page_callback_.is_null()); |
| fake_open_feedback_page_callback_ = fake_open_feedback_page_callback; |
| } |
| |
| void QuickAnswersUiController::OpenFeedbackPage( |
| const std::string& feedback_template) { |
| if (!fake_open_feedback_page_callback_.is_null()) { |
| CHECK_IS_TEST(); |
| fake_open_feedback_page_callback_.Run(feedback_template); |
| return; |
| } |
| |
| // TODO(b/229007013): Merge the logics after resolve the deps cycle with |
| // //c/b/ui in ash chrome build. |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| ash::NewWindowDelegate::GetPrimary()->OpenFeedbackPage( |
| ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceQuickAnswers, |
| feedback_template); |
| #elif BUILDFLAG(IS_CHROMEOS_LACROS) |
| chrome::OpenFeedbackDialog( |
| chrome::FindBrowserWithActiveWindow(), |
| feedback::FeedbackSource::kFeedbackSourceQuickAnswers, feedback_template); |
| #endif |
| } |
| |
| void QuickAnswersUiController::SetFakeOpenWebUrlForTesting( |
| QuickAnswersUiController::FakeOpenWebUrlCallback |
| fake_open_web_url_callback) { |
| CHECK_IS_TEST(); |
| CHECK(!fake_open_web_url_callback.is_null()); |
| CHECK(fake_open_web_url_callback_.is_null()); |
| fake_open_web_url_callback_ = fake_open_web_url_callback; |
| } |
| |
| void QuickAnswersUiController::OpenWebUrl(const GURL& url) { |
| if (!fake_open_web_url_callback_.is_null()) { |
| CHECK_IS_TEST(); |
| fake_open_web_url_callback_.Run(url); |
| return; |
| } |
| |
| OpenUrl(profile_, url); |
| } |
| |
| void QuickAnswersUiController::OnUserConsentNoThanksPressed() { |
| OnUserConsentResult(false); |
| } |
| |
| void QuickAnswersUiController::OnUserConsentAllowPressed() { |
| // When user consent is accepted, `QuickAnswersView` will be displayed instead |
| // of dismissing the menu. |
| GetReadWriteCardsUiController() |
| .pre_target_handler() |
| .set_dismiss_anchor_menu_on_view_closed(false); |
| |
| OnUserConsentResult(true); |
| } |
| |
| void QuickAnswersUiController::OnUserConsentResult(bool consented) { |
| DCHECK(IsShowingUserConsentView()); |
| controller_->OnUserConsentResult(consented); |
| |
| if (consented && IsShowingQuickAnswersView()) |
| quick_answers_view()->RequestFocus(); |
| } |
| |
| bool QuickAnswersUiController::IsShowingUserConsentView() const { |
| if (user_consent_view_) { |
| CHECK_EQ(controller_->GetQuickAnswersVisibility(), |
| QuickAnswersVisibility::kUserConsentVisible); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool QuickAnswersUiController::IsShowingQuickAnswersView() const { |
| if (quick_answers_view_) { |
| CHECK_EQ(controller_->GetQuickAnswersVisibility(), |
| QuickAnswersVisibility::kQuickAnswersVisible); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool QuickAnswersUiController::IsShowingRichAnswersView() const { |
| return rich_answers_widget_ && !rich_answers_widget_->IsClosed() && |
| rich_answers_widget_->GetContentsView(); |
| } |
| |
| chromeos::ReadWriteCardsUiController& |
| QuickAnswersUiController::GetReadWriteCardsUiController() const { |
| return controller_->read_write_cards_ui_controller(); |
| } |