// 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 "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.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/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/components/editor_menu/public/cpp/editor_helpers.h"
#include "chromeos/constants/chromeos_features.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";
}
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::BindEditorPanelManager(
mojo::PendingReceiver<crosapi::mojom::EditorPanelManager>
pending_receiver) {
panel_manager_.BindReceiver(std::move(pending_receiver));
}
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(const 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 EditorMode::kRewrite:
mako_bubble_coordinator_.LoadEditorUI(
profile_, MakoEditorMode::kRewrite,
/*can_fallback_to_center_position=*/true,
/*feedback_enabled=*/editor_switch_->IsFeedbackEnabled(),
active_query_context.preset_query_id,
active_query_context.freeform_text);
query_context_ = std::nullopt;
metrics_recorder_->LogEditorState(EditorStates::kNativeRequest);
break;
case EditorMode::kWrite:
mako_bubble_coordinator_.LoadEditorUI(
profile_, MakoEditorMode::kWrite,
/*can_fallback_to_center_position=*/true,
/*feedback_enabled=*/editor_switch_->IsFeedbackEnabled(),
active_query_context.preset_query_id,
active_query_context.freeform_text);
query_context_ = std::nullopt;
metrics_recorder_->LogEditorState(EditorStates::kNativeRequest);
break;
case EditorMode::kConsentNeeded:
query_context_ = EditorQueryContext(/*preset_query_id=*/preset_query_id,
/*freeform_text=*/freeform_text);
if (chromeos::features::IsMagicBoostEnabled()) {
crosapi::CrosapiManager::Get()
->crosapi_ash()
->magic_boost_controller_ash()
->ShowDisclaimerUi(
/*display_id=*/display::Screen::GetScreen()
->GetPrimaryDisplay()
.id(),
/*action=*/
crosapi::mojom::MagicBoostController::TransitionAction::
kShowEditorPanel,
/*opt_in_features=*/OptInFeatures::kOrcaAndHmr);
} else {
mako_bubble_coordinator_.LoadConsentUI(profile_);
}
metrics_recorder_->LogEditorState(EditorStates::kConsentScreenImpression);
break;
case EditorMode::kHardBlocked:
case EditorMode::kSoftBlocked:
mako_bubble_coordinator_.CloseUI();
}
}
void EditorMediator::CacheContext() {
GetTextFieldContextualInfo(
base::BindOnce(&EditorMediator::OnTextFieldContextualInfoChanged,
weak_ptr_factory_.GetWeakPtr()));
mako_bubble_coordinator_.CacheContextCaretBounds();
size_t selected_length =
chromeos::editor_helpers::NonWhitespaceAndSymbolsLength(
surrounding_text_.text, surrounding_text_.selection_range);
editor_context_.OnTextSelectionLengthChanged(selected_length);
if (IsServiceConnected()) {
service_connection_->editor_event_proxy()->OnSurroundingTextChanged(
surrounding_text_.text, surrounding_text_.selection_range);
}
}
void EditorMediator::FetchAndUpdateInputContextForTesting() {
GetTextFieldContextualInfo(
base::BindOnce(&EditorMediator::OnTextFieldContextualInfoChanged,
weak_ptr_factory_.GetWeakPtr()));
}
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();
}
EditorMode EditorMediator::GetEditorMode() const {
if (editor_mode_override_for_testing_.has_value()) {
return *editor_mode_override_for_testing_;
}
return editor_switch_->GetEditorMode();
}
ConsentStatus 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(EditorMode editor_mode) {
editor_mode_override_for_testing_ = editor_mode;
}
} // namespace ash::input_method