| // Copyright 2023 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/ash/input_method/editor_mediator.h" |
| |
| #include <optional> |
| #include <string_view> |
| |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/shell.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/strings/utf_offset_string_conversions.h" |
| #include "chrome/browser/ash/input_method/editor_consent_enums.h" |
| #include "chrome/browser/ash/input_method/editor_geolocation_provider.h" |
| #include "chrome/browser/ash/input_method/editor_metrics_enums.h" |
| #include "chrome/browser/ash/input_method/editor_metrics_recorder.h" |
| #include "chrome/browser/ash/input_method/editor_query_context.h" |
| #include "chrome/browser/ash/input_method/editor_text_query_from_manta.h" |
| #include "chrome/browser/ash/input_method/editor_text_query_from_memory.h" |
| #include "chrome/browser/ash/input_method/editor_text_query_provider.h" |
| #include "chrome/browser/ash/input_method/editor_transition_enums.h" |
| #include "chrome/browser/ash/magic_boost/magic_boost_controller_ash.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/ui/webui/ash/mako/mako_bubble_coordinator.h" |
| #include "chromeos/ash/components/editor_menu/public/cpp/editor_consent_status.h" |
| #include "chromeos/ash/components/editor_menu/public/cpp/editor_helpers.h" |
| #include "chromeos/ash/components/editor_menu/public/cpp/editor_mode.h" |
| #include "chromeos/ash/components/editor_menu/public/cpp/editor_text_selection_mode.h" |
| #include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h" |
| #include "ui/base/ime/ash/ime_bridge.h" |
| #include "ui/display/screen.h" |
| #include "ui/display/tablet_state.h" |
| |
| namespace ash::input_method { |
| namespace { |
| |
| constexpr std::u16string_view kAnnouncementViewName = u"Orca"; |
| |
| std::optional<orca::mojom::ContextPtr> CreateContext(const std::u16string& text, |
| gfx::Range range) { |
| std::vector<size_t> offsets = {range.start(), range.end()}; |
| |
| const std::string text_utf8 = |
| base::UTF16ToUTF8AndAdjustOffsets(text, &offsets); |
| |
| // In some rare cases such as crbug.com/425874277, the utf16 to utf8 conversion is not |
| // successful. |
| if (offsets[0] == std::u16string::npos || |
| offsets[1] == std::u16string::npos) { |
| LOG(ERROR) << "The selection range is not valid"; |
| return std::nullopt; |
| } |
| |
| auto context = orca::mojom::Context::New(); |
| |
| context->surrounding_text = orca::mojom::SurroundingText::New( |
| text_utf8, gfx::Range(offsets[0], offsets[1])); |
| |
| return context; |
| } |
| |
| crosapi::mojom::MagicBoostController::TransitionAction |
| ConvertToMagicBoostTransitionAction(EditorNoticeTransitionAction action) { |
| switch (action) { |
| case EditorNoticeTransitionAction::kDoNothing: |
| return crosapi::mojom::MagicBoostController::TransitionAction::kDoNothing; |
| case EditorNoticeTransitionAction::kShowEditorPanel: |
| return crosapi::mojom::MagicBoostController::TransitionAction:: |
| kShowEditorPanel; |
| } |
| } |
| |
| } // namespace |
| |
| EditorMediator::EditorMediator( |
| Profile* profile, |
| std::unique_ptr<EditorGeolocationProvider> editor_geolocation_provider) |
| : profile_(profile), |
| panel_manager_(this), |
| editor_geolocation_provider_(std::move(editor_geolocation_provider)), |
| editor_context_(this, this, editor_geolocation_provider_.get()), |
| editor_switch_( |
| std::make_unique<EditorSwitch>(this, profile, &editor_context_)), |
| metrics_recorder_( |
| std::make_unique<EditorMetricsRecorder>(&editor_context_, |
| GetEditorOpportunityMode())), |
| consent_store_( |
| std::make_unique<EditorConsentStore>(profile->GetPrefs(), |
| metrics_recorder_.get())), |
| announcer_(kAnnouncementViewName) { |
| editor_context_.OnTabletModeUpdated( |
| display::Screen::GetScreen()->InTabletMode()); |
| } |
| |
| EditorMediator::~EditorMediator() = default; |
| |
| void EditorMediator::BindEditorClient( |
| mojo::PendingReceiver<orca::mojom::EditorClient> pending_receiver) { |
| if (IsServiceConnected()) { |
| service_connection_->editor_client_connector()->BindEditorClient( |
| std::move(pending_receiver)); |
| } |
| } |
| |
| void EditorMediator::OnEditorServiceConnected(bool is_connection_successful) {} |
| |
| bool EditorMediator::IsServiceConnected() { |
| return editor_service_connector_ && editor_service_connector_->IsBound() && |
| service_connection_; |
| } |
| |
| void EditorMediator::ResetEditorConnections() { |
| if (editor_service_connector_) { |
| service_connection_ = std::make_unique<ServiceConnection>( |
| profile_, this, metrics_recorder_.get(), |
| editor_service_connector_.get()); |
| panel_manager_.BindEditorClient(); |
| } |
| } |
| |
| void EditorMediator::OnContextUpdated() { |
| editor_switch_->OnContextUpdated(); |
| } |
| |
| void EditorMediator::OnImeChange(std::string_view engine_id) { |
| if (base::FeatureList::IsEnabled(ash::features::kOrcaServiceConnection) && |
| service_connection_) { |
| ResetEditorConnections(); |
| } |
| } |
| |
| std::optional<ukm::SourceId> EditorMediator::GetUkmSourceId() { |
| TextInputTarget* text_input = IMEBridge::Get()->GetInputContextHandler(); |
| if (!text_input) { |
| return std::nullopt; |
| } |
| |
| ukm::SourceId source_id = text_input->GetClientSourceForMetrics(); |
| if (source_id == ukm::kInvalidSourceId) { |
| return std::nullopt; |
| } |
| return source_id; |
| } |
| |
| void EditorMediator::OnFocus(int context_id) { |
| if (mako_bubble_coordinator_.IsShowingUI() || |
| panel_manager_.IsEditorMenuVisible()) { |
| return; |
| } |
| |
| if (IsAllowedForUse() && !editor_service_connector_) { |
| editor_service_connector_ = |
| std::make_unique<EditorServiceConnector>(&editor_context_); |
| ResetEditorConnections(); |
| } |
| |
| if (IsServiceConnected()) { |
| service_connection_->system_actuator()->OnFocus(context_id); |
| } |
| } |
| |
| void EditorMediator::OnBlur() { |
| if (mako_bubble_coordinator_.IsShowingUI() || |
| panel_manager_.IsEditorMenuVisible()) { |
| return; |
| } |
| } |
| |
| void EditorMediator::OnActivateIme(std::string_view engine_id) { |
| editor_context_.OnActivateIme(engine_id); |
| |
| if (base::FeatureList::IsEnabled(ash::features::kOrcaServiceConnection) && |
| IsServiceConnected()) { |
| ResetEditorConnections(); |
| } |
| } |
| |
| void EditorMediator::OnDisplayTabletStateChanged(display::TabletState state) { |
| switch (state) { |
| case display::TabletState::kInClamshellMode: |
| editor_context_.OnTabletModeUpdated(/*tablet_mode_enabled=*/false); |
| break; |
| case display::TabletState::kEnteringTabletMode: |
| editor_context_.OnTabletModeUpdated(/*tablet_mode_enabled=*/true); |
| if (mako_bubble_coordinator_.IsShowingUI()) { |
| mako_bubble_coordinator_.CloseUI(); |
| } |
| break; |
| case display::TabletState::kInTabletMode: |
| case display::TabletState::kExitingTabletMode: |
| break; |
| } |
| } |
| |
| void EditorMediator::OnSurroundingTextChanged(const std::u16string& text, |
| gfx::Range selection_range) { |
| if (mako_bubble_coordinator_.IsShowingUI() || |
| panel_manager_.IsEditorMenuVisible()) { |
| return; |
| } |
| |
| surrounding_text_ = {.text = text, .selection_range = selection_range}; |
| } |
| |
| void EditorMediator::Announce(const std::u16string& message) { |
| announcer_.Announce(message); |
| } |
| |
| void EditorMediator::ProcessConsentAction(ConsentAction consent_action) { |
| consent_store_->ProcessConsentAction(consent_action); |
| } |
| |
| void EditorMediator::ShowUI() { |
| mako_bubble_coordinator_.ShowUI(); |
| } |
| |
| void EditorMediator::CloseUI() { |
| mako_bubble_coordinator_.CloseUI(); |
| } |
| |
| size_t EditorMediator::GetSelectedTextLength() { |
| return surrounding_text_.selection_range.length(); |
| } |
| |
| void EditorMediator::OnEditorModeChanged( |
| chromeos::editor_menu::EditorMode mode) { |
| panel_manager_.NotifyEditorModeChanged(mode); |
| } |
| |
| void EditorMediator::OnPromoCardDeclined() { |
| consent_store_->ProcessPromoCardAction(PromoCardAction::kDecline); |
| } |
| |
| void EditorMediator::HandleTrigger( |
| std::optional<std::string_view> preset_query_id, |
| std::optional<std::string_view> freeform_text) { |
| metrics_recorder_->SetTone(preset_query_id, freeform_text); |
| |
| EditorQueryContext active_query_context = |
| query_context_.has_value() |
| ? EditorQueryContext{query_context_->preset_query_id, |
| query_context_->freeform_text} |
| : EditorQueryContext{preset_query_id, freeform_text}; |
| |
| switch (GetEditorMode()) { |
| case chromeos::editor_menu::EditorMode::kRewrite: |
| mako_bubble_coordinator_.LoadEditorUI( |
| profile_, MakoEditorMode::kRewrite, |
| /*can_fallback_to_center_position=*/true, |
| /*feedback_enabled=*/CanAccessFeedback(), |
| active_query_context.preset_query_id, |
| active_query_context.freeform_text); |
| query_context_ = std::nullopt; |
| metrics_recorder_->LogEditorState(EditorStates::kNativeRequest); |
| break; |
| case chromeos::editor_menu::EditorMode::kWrite: |
| mako_bubble_coordinator_.LoadEditorUI( |
| profile_, MakoEditorMode::kWrite, |
| /*can_fallback_to_center_position=*/true, |
| /*feedback_enabled=*/CanAccessFeedback(), |
| active_query_context.preset_query_id, |
| active_query_context.freeform_text); |
| query_context_ = std::nullopt; |
| metrics_recorder_->LogEditorState(EditorStates::kNativeRequest); |
| break; |
| case chromeos::editor_menu::EditorMode::kConsentNeeded: |
| query_context_ = EditorQueryContext(/*preset_query_id=*/preset_query_id, |
| /*freeform_text=*/freeform_text); |
| ShowNotice(EditorNoticeTransitionAction::kShowEditorPanel); |
| metrics_recorder_->LogEditorState(EditorStates::kConsentScreenImpression); |
| break; |
| case chromeos::editor_menu::EditorMode::kHardBlocked: |
| case chromeos::editor_menu::EditorMode::kSoftBlocked: |
| mako_bubble_coordinator_.CloseUI(); |
| } |
| } |
| |
| void EditorMediator::ShowNotice( |
| EditorNoticeTransitionAction transition_action) { |
| if (chromeos::MagicBoostState::Get()->IsUserEligibleForGenAIFeatures()) { |
| ash::MagicBoostControllerAsh::Get()->ShowDisclaimerUi( |
| /*display_id=*/display::Screen::GetScreen()->GetPrimaryDisplay().id(), |
| /*action=*/ |
| ConvertToMagicBoostTransitionAction(transition_action), |
| /*opt_in_features=*/OptInFeatures::kOrcaAndHmr); |
| return; |
| } |
| |
| if (IsServiceConnected()) { |
| service_connection_->system_actuator()->SetNoticeTransitionAction( |
| transition_action); |
| } |
| |
| mako_bubble_coordinator_.LoadConsentUI(profile_); |
| } |
| |
| void EditorMediator::CacheContext() { |
| OnTextFieldContextualInfoChanged(GetTextFieldContextualInfo()); |
| |
| mako_bubble_coordinator_.CacheContextCaretBounds(); |
| |
| size_t non_whitespace_selected_text_length = |
| chromeos::editor_helpers::NonWhitespaceAndSymbolsLength( |
| surrounding_text_.text, surrounding_text_.selection_range); |
| |
| auto context = |
| CreateContext(surrounding_text_.text, surrounding_text_.selection_range); |
| |
| EditorTextSelection text_selection = { |
| .is_valid_selection = (context != std::nullopt), |
| .non_whitespace_selected_text_length = |
| non_whitespace_selected_text_length, |
| }; |
| |
| editor_context_.OnTextSelectionChanged(text_selection); |
| |
| if (IsServiceConnected() && context.has_value()) { |
| service_connection_->editor_event_proxy()->OnSurroundingTextChanged( |
| std::move(context.value())); |
| } |
| } |
| |
| void EditorMediator::FetchAndUpdateInputContextForTesting() { |
| OnTextFieldContextualInfoChanged(GetTextFieldContextualInfo()); |
| } |
| |
| EditorMediator::ServiceConnection::ServiceConnection( |
| Profile* profile, |
| EditorMediator* mediator, |
| EditorMetricsRecorder* metrics_recorder, |
| EditorServiceConnector* service_connector) { |
| mojo::PendingAssociatedRemote<orca::mojom::SystemActuator> |
| system_actuator_remote; |
| mojo::PendingAssociatedRemote<orca::mojom::TextQueryProvider> |
| text_query_provider_remote; |
| mojo::PendingAssociatedReceiver<orca::mojom::EditorClientConnector> |
| editor_client_connector_receiver; |
| mojo::PendingAssociatedReceiver<orca::mojom::EditorEventSink> |
| editor_event_sink_receiver; |
| |
| system_actuator_ = std::make_unique<EditorSystemActuator>( |
| profile, system_actuator_remote.InitWithNewEndpointAndPassReceiver(), |
| mediator); |
| text_query_provider_ = std::make_unique<EditorTextQueryProvider>( |
| text_query_provider_remote.InitWithNewEndpointAndPassReceiver(), |
| metrics_recorder, std::make_unique<EditorTextQueryFromManta>(profile)); |
| editor_client_connector_ = std::make_unique<EditorClientConnector>( |
| editor_client_connector_receiver.InitWithNewEndpointAndPassRemote()); |
| editor_event_proxy_ = std::make_unique<EditorEventProxy>( |
| editor_event_sink_receiver.InitWithNewEndpointAndPassRemote()); |
| |
| service_connector->BindEditor(std::move(editor_client_connector_receiver), |
| std::move(editor_event_sink_receiver), |
| std::move(system_actuator_remote), |
| std::move(text_query_provider_remote)); |
| } |
| |
| EditorMediator::ServiceConnection::~ServiceConnection() = default; |
| |
| EditorEventProxy* EditorMediator::ServiceConnection::editor_event_proxy() { |
| return editor_event_proxy_.get(); |
| } |
| |
| EditorClientConnector* |
| EditorMediator::ServiceConnection::editor_client_connector() { |
| return editor_client_connector_.get(); |
| } |
| |
| EditorTextQueryProvider* |
| EditorMediator::ServiceConnection::text_query_provider() { |
| return text_query_provider_.get(); |
| } |
| |
| EditorSystemActuator* EditorMediator::ServiceConnection::system_actuator() { |
| return system_actuator_.get(); |
| } |
| |
| void EditorMediator::OnTextFieldContextualInfoChanged( |
| const TextFieldContextualInfo& info) { |
| editor_context_.OnInputContextUpdated( |
| IMEBridge::Get()->GetCurrentInputContext(), info); |
| |
| if (IsServiceConnected()) { |
| service_connection_->system_actuator()->OnInputContextUpdated(info.tab_url); |
| } |
| } |
| |
| bool EditorMediator::IsAllowedForUse() { |
| return editor_switch_->IsAllowedForUse(); |
| } |
| |
| bool EditorMediator::CanAccessFeedback() { |
| return editor_switch_->IsFeedbackEnabled(); |
| } |
| |
| bool EditorMediator::CanShowNoticeBanner() const { |
| return editor_switch_->CanShowNoticeBanner(); |
| } |
| |
| chromeos::editor_menu::EditorMode EditorMediator::GetEditorMode() const { |
| if (editor_mode_override_for_testing_.has_value()) { |
| return *editor_mode_override_for_testing_; |
| } |
| return editor_switch_->GetEditorMode(); |
| } |
| |
| chromeos::editor_menu::EditorTextSelectionMode |
| EditorMediator::GetEditorTextSelectionMode() const { |
| return editor_switch_->GetEditorTextSelectionMode(); |
| } |
| |
| chromeos::editor_menu::EditorConsentStatus EditorMediator::GetConsentStatus() |
| const { |
| return consent_store_->GetConsentStatus(); |
| } |
| |
| EditorMetricsRecorder* EditorMediator::GetMetricsRecorder() { |
| return metrics_recorder_.get(); |
| } |
| |
| EditorOpportunityMode EditorMediator::GetEditorOpportunityMode() const { |
| return editor_switch_->GetEditorOpportunityMode(); |
| } |
| |
| std::vector<EditorBlockedReason> EditorMediator::GetBlockedReasons() const { |
| return editor_switch_->GetBlockedReasons(); |
| } |
| |
| void EditorMediator::Shutdown() { |
| // Note that this method is part of the two-phase shutdown completed by a |
| // KeyedService. This method is invoked as the first phase, and is called |
| // prior to the destruction of the keyed profile (this allows us to cleanup |
| // any resources that depend on a valid profile instance - ie WebUI). The |
| // second phase is the destruction of the eKeyedService itself. |
| mako_bubble_coordinator_.CloseUI(); |
| profile_ = nullptr; |
| consent_store_ = nullptr; |
| editor_switch_ = nullptr; |
| } |
| |
| bool EditorMediator::SetTextQueryProviderResponseForTesting( |
| const std::vector<std::string>& mock_results) { |
| if (!IsServiceConnected()) { |
| return false; |
| } |
| service_connection_->text_query_provider()->SetProvider( |
| std::make_unique<EditorTextQueryFromMemory>(mock_results)); // IN-TEST |
| return true; |
| } |
| |
| void EditorMediator::OverrideEditorModeForTesting( |
| chromeos::editor_menu::EditorMode editor_mode) { |
| editor_mode_override_for_testing_ = editor_mode; |
| } |
| |
| } // namespace ash::input_method |