chromium/chrome/browser/ui/chromeos/read_write_cards/read_write_cards_manager_impl.cc

// 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/ui/chromeos/read_write_cards/read_write_cards_manager_impl.h"

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/command_line.h"
#include "base/functional/callback.h"
#include "base/hash/sha1.h"
#include "base/types/expected.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/chromeos/magic_boost/magic_boost_card_controller.h"
#include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h"
#include "chrome/browser/ui/views/editor_menu/editor_menu_controller_impl.h"
#include "chrome/browser/ui/views/editor_menu/utils/editor_types.h"
#include "chrome/browser/ui/views/mahi/mahi_menu_controller.h"
#include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
#include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h"
#include "chromeos/components/quick_answers/quick_answers_client.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/magic_boost.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/context_menu_params.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/blink/public/mojom/context_menu/context_menu.mojom-shared.h"

namespace chromeos {

using OptInFeatures = crosapi::mojom::MagicBoostController::OptInFeatures;

ReadWriteCardsManagerImpl::ReadWriteCardsManagerImpl()
    : quick_answers_controller_(
          std::make_unique<QuickAnswersControllerImpl>(ui_controller_)) {
  quick_answers_controller_->SetClient(
      std::make_unique<quick_answers::QuickAnswersClient>(
          g_browser_process->shared_url_loader_factory(),
          quick_answers_controller_->GetQuickAnswersDelegate()));

  if (chromeos::features::IsOrcaEnabled()) {
    editor_menu_controller_ =
        std::make_unique<editor_menu::EditorMenuControllerImpl>();
  }

  if (chromeos::features::IsMahiEnabled()) {
    mahi_menu_controller_.emplace(ui_controller_);
  }

  if (chromeos::features::IsMagicBoostEnabled()) {
    magic_boost_card_controller_.emplace();
  }
}

ReadWriteCardsManagerImpl::~ReadWriteCardsManagerImpl() = default;

void ReadWriteCardsManagerImpl::FetchController(
    const content::ContextMenuParams& params,
    content::BrowserContext* context,
    editor_menu::FetchControllersCallback callback) {
  // Skip password input field.
  const bool is_password_field =
      params.form_control_type == blink::mojom::FormControlType::kInputPassword;
  if (is_password_field) {
    std::move(callback).Run({});
    return;
  }

  if (!editor_menu_controller_) {
    std::move(callback).Run(GetQuickAnswersAndMahiControllers(params));
    return;
  }

  editor_menu_controller_->GetEditorContext(
      base::BindOnce(&ReadWriteCardsManagerImpl::OnGetEditorContext,
                     weak_factory_.GetWeakPtr(), params, std::move(callback)));
}

void ReadWriteCardsManagerImpl::SetContextMenuBounds(
    const gfx::Rect& context_menu_bounds) {
  ui_controller_.SetContextMenuBounds(context_menu_bounds);
}

void ReadWriteCardsManagerImpl::TryCreatingEditorSession(
    const content::ContextMenuParams& params,
    content::BrowserContext* context) {
  if (editor_menu_controller_) {
    editor_menu_controller_->SetBrowserContext(context);
    editor_menu_controller_->TryCreatingEditorSession();
  }
}

void ReadWriteCardsManagerImpl::OnGetEditorContext(
    const content::ContextMenuParams& params,
    editor_menu::FetchControllersCallback callback,
    const editor_menu::EditorContext& editor_context) {
  std::move(callback).Run(GetControllers(params, editor_context));
}

std::vector<base::WeakPtr<chromeos::ReadWriteCardController>>
ReadWriteCardsManagerImpl::GetControllers(
    const content::ContextMenuParams& params,
    const editor_menu::EditorContext& editor_context) {
  const bool should_show_editor_menu =
      editor_menu_controller_ && params.is_editable;
  auto opt_in_features = GetMagicBoostOptInFeatures(params, editor_context);

  if (opt_in_features) {
    crosapi::mojom::MagicBoostController::TransitionAction action =
        crosapi::mojom::MagicBoostController::TransitionAction::kDoNothing;

    // Calculate the action to take after the opt-in flow.
    if (should_show_editor_menu) {
      action = crosapi::mojom::MagicBoostController::TransitionAction::
          kShowEditorPanel;
    } else if (ShouldShowMahi(params)) {
      action =
          crosapi::mojom::MagicBoostController::TransitionAction::kShowHmrPanel;
    }

    // Always set the transition action to handle the edge case that this code
    // path is hit more than once with different actions.
    magic_boost_card_controller_->set_transition_action(action);

    magic_boost_card_controller_->SetOptInFeature(opt_in_features.value());

    return {magic_boost_card_controller_->GetWeakPtr()};
  }

  // If Magic Boost is not enabled, each feature (besides Mahi which only uses
  // Magic Boost) will have its own opt-in flow, provided within each individual
  // controller.
  if (should_show_editor_menu) {
    // Use editor menu if available.
    if (editor_context.mode != editor_menu::EditorMode::kHardBlocked &&
        editor_context.mode != editor_menu::EditorMode::kSoftBlocked) {
      return {editor_menu_controller_->GetWeakPtr()};
    }

    editor_menu_controller_->LogEditorMode(editor_context.mode);
  }

  // Otherwise, use Quick Answers and Mahi if available.

  base::expected<HMRConsentStatus, MagicBoostState::Error> hmr_consent_status =
      base::unexpected(MagicBoostState::Error::kUninitialized);
  if (chromeos::features::IsMagicBoostEnabled()) {
    hmr_consent_status = MagicBoostState::Get()->hmr_consent_status();

    // Ensure the disclaimer view is closed before moving to the next step
    magic_boost_card_controller_->CloseDisclaimerUi();
  }

  // Return no controller if consent_status is `kDeclined` (users explicitly
  // decline in the opt-in flow), or `kUnset` (both Quick Answers and Mahi is
  // not available to opt-in).
  if (hmr_consent_status == HMRConsentStatus::kDeclined ||
      hmr_consent_status == HMRConsentStatus::kUnset) {
    return {};
  }

  if (hmr_consent_status.has_value()) {
    CHECK(hmr_consent_status == HMRConsentStatus::kApproved ||
          hmr_consent_status == HMRConsentStatus::kPendingDisclaimer);
  }

  return GetQuickAnswersAndMahiControllers(params);
}

std::vector<base::WeakPtr<chromeos::ReadWriteCardController>>
ReadWriteCardsManagerImpl::GetQuickAnswersAndMahiControllers(
    const content::ContextMenuParams& params) {
  std::vector<base::WeakPtr<chromeos::ReadWriteCardController>> controllers;

  if (ShouldShowQuickAnswers(params)) {
    controllers.emplace_back(quick_answers_controller_->GetWeakPtr());
  }

  if (mahi_menu_controller_) {
    mahi_menu_controller_->RecordPageDistillable();
    if (ShouldShowMahi(params)) {
      controllers.emplace_back(mahi_menu_controller_->GetWeakPtr());
    }
  }

  return controllers;
}

bool ReadWriteCardsManagerImpl::ShouldShowQuickAnswers(
    const content::ContextMenuParams& params) {
  // Display Quick Answers card if it is eligible and there's selected text.
  return QuickAnswersState::IsEligible() && !params.selection_text.empty() &&
         quick_answers_controller_;
}

bool ReadWriteCardsManagerImpl::ShouldShowMahi(
    const content::ContextMenuParams& params) {
  return chromeos::features::IsMahiEnabled() && mahi_menu_controller_ &&
         mahi_menu_controller_->IsFocusedPageDistillable();
}

std::optional<OptInFeatures>
ReadWriteCardsManagerImpl::GetMagicBoostOptInFeatures(
    const content::ContextMenuParams& params,
    const editor_menu::EditorContext& editor_context) {
  if (!magic_boost_card_controller_) {
    return std::nullopt;
  }

  // Check if we should go through Magic Boost opt-in flow when we should show
  // Editor card.
  const bool should_show_editor_menu =
      editor_menu_controller_ && params.is_editable;

  // Only opt in orca if it is not blocked by any hard requirements and its
  // current status is unset.
  const bool should_opt_in_orca =
      editor_context.mode != editor_menu::EditorMode::kHardBlocked &&
      !editor_context.consent_status_settled;

  if (should_show_editor_menu) {
    if (should_opt_in_orca) {
      // We should opt in both Orca and HMR if we are opting-in Orca.
      return OptInFeatures::kOrcaAndHmr;
    }

    return std::nullopt;
  }

  // Check if we should go through Magic Boost opt-in flow when we should show
  // Quick Answers and/or Mahi card.
  base::expected<HMRConsentStatus, MagicBoostState::Error> hmr_consent_status =
      base::unexpected(MagicBoostState::Error::kUninitialized);
  if (chromeos::features::IsMagicBoostEnabled()) {
    hmr_consent_status = MagicBoostState::Get()->hmr_consent_status();
  }

  if ((ShouldShowQuickAnswers(params) || ShouldShowMahi(params)) &&
      hmr_consent_status == HMRConsentStatus::kUnset) {
    return should_opt_in_orca ? OptInFeatures::kOrcaAndHmr
                              : OptInFeatures::kHmrOnly;
  }

  return std::nullopt;
}

}  // namespace chromeos