910e62b5创建于 1月15日历史提交
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/scanner/scanner_controller.h"

#include <cstddef>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/scanner/scanner_delegate.h"
#include "ash/public/cpp/scanner/scanner_feedback_info.h"
#include "ash/public/cpp/scanner/scanner_profile_scoped_delegate.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/scanner/scanner_action_handler.h"
#include "ash/scanner/scanner_action_view_model.h"
#include "ash/scanner/scanner_command_delegate_impl.h"
#include "ash/scanner/scanner_enterprise_policy.h"
#include "ash/scanner/scanner_feedback.h"
#include "ash/scanner/scanner_metrics.h"
#include "ash/scanner/scanner_session.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/screen_pinning_controller.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_view_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromeos/ash/components/specialized_features/feature_access_checker.h"
#include "components/account_id/account_id.h"
#include "components/feedback/feedback_constants.h"
#include "components/manta/proto/scanner.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/public/cpp/notifier_id.h"

namespace ash {

namespace {

using enum ScannerFeatureUserState;

constexpr char kScannerActionNotificationId[] = "scanner_action_notification";
constexpr char kScannerNotifierId[] = "ash.scanner";

constexpr char kScannerActionSuccessToastId[] = "scanner_action_success";
constexpr char kScannerActionFailureToastId[] = "scanner_action_failure";

constexpr size_t kUserFacingStringDepthLimit = 20;
constexpr size_t kUserFacingStringOutputLimit =
    std::numeric_limits<size_t>::max();

std::u16string GetTitleForActionProgressNotification(
    manta::proto::ScannerAction::ActionCase action_case) {
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_PROGRESS_TITLE_ADDING_EVENT);
    case manta::proto::ScannerAction::kNewContact:
    case manta::proto::ScannerAction::kNewGoogleDoc:
    case manta::proto::ScannerAction::kNewGoogleSheet:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_PROGRESS_TITLE_CREATING);
    case manta::proto::ScannerAction::kCopyToClipboard:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_PROGRESS_TITLE_COPYING);
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      NOTREACHED();
  }
}

std::u16string GetDisplaySourceForActionProgressNotification(
    manta::proto::ScannerAction::ActionCase action_case) {
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      return l10n_util::GetStringUTF16(IDS_ASH_SCANNER_ACTION_NEW_EVENT_SOURCE);
    case manta::proto::ScannerAction::kNewContact:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_NEW_CONTACT_SOURCE);
    case manta::proto::ScannerAction::kNewGoogleDoc:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_NEW_GOOGLE_DOC_SOURCE);
    case manta::proto::ScannerAction::kNewGoogleSheet:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_NEW_GOOGLE_SHEET_SOURCE);
    case manta::proto::ScannerAction::kCopyToClipboard:
      return l10n_util::GetStringUTF16(IDS_ASH_SCANNER_ACTION_COPY_TEXT_SOURCE);
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      NOTREACHED();
  }
}

std::u16string GetToastMessageForActionSuccess(
    manta::proto::ScannerAction::ActionCase action_case) {
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_EVENT);
    case manta::proto::ScannerAction::kNewContact:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_CONTACT);
    case manta::proto::ScannerAction::kNewGoogleDoc:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_DOC);
    case manta::proto::ScannerAction::kNewGoogleSheet:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_SHEET);
    case manta::proto::ScannerAction::kCopyToClipboard:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_COPY_TEXT_AND_FORMAT);
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      NOTREACHED();
  }
}

std::u16string GetToastMessageForActionFailure(
    manta::proto::ScannerAction::ActionCase action_case) {
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_EVENT);
    case manta::proto::ScannerAction::kNewContact:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_CONTACT);
    case manta::proto::ScannerAction::kNewGoogleDoc:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_DOC);
    case manta::proto::ScannerAction::kNewGoogleSheet:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_SHEET);
    case manta::proto::ScannerAction::kCopyToClipboard:
      return l10n_util::GetStringUTF16(
          IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_COPY_TEXT_AND_FORMAT);
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      NOTREACHED();
  }
}

// Shows an action progress notification. The new notification will remove the
// previous action notification if there was one.
void ShowActionProgressNotification(
    const ScannerActionViewModel& scanner_action) {
  message_center::RichNotificationData optional_fields;
  // Show an infinite loading progress bar.
  optional_fields.progress = -1;
  optional_fields.never_timeout = true;
  optional_fields.pinned = true;

  auto* message_center = message_center::MessageCenter::Get();
  message_center->RemoveNotification(kScannerActionNotificationId,
                                     /*by_user=*/false);
  manta::proto::ScannerAction::ActionCase action_case =
      scanner_action.GetActionCase();
  std::unique_ptr<message_center::Notification> notification =
      CreateSystemNotificationPtr(
          message_center::NOTIFICATION_TYPE_PROGRESS,
          kScannerActionNotificationId,
          /*title=*/GetTitleForActionProgressNotification(action_case),
          /*message=*/u"",
          /*display_source=*/
          GetDisplaySourceForActionProgressNotification(action_case), GURL(),
          message_center::NotifierId(
              message_center::NotifierType::SYSTEM_COMPONENT,
              kScannerNotifierId, NotificationCatalogName::kScannerAction),
          optional_fields, /*delegate=*/nullptr, scanner_action.GetIcon(),
          message_center::SystemNotificationWarningLevel::NORMAL);
  notification->SetSystemPriority();
  message_center->AddNotification(std::move(notification));
}

void RecordExecutePopulatedActionTimer(
    manta::proto::ScannerAction::ActionCase action_case,
    base::TimeTicks execute_start_time) {
  // TODO(b/363101363): Add tests.
  std::string_view variant_name;
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      variant_name = kScannerFeatureTimerExecutePopulatedNewCalendarEventAction;
      break;
    case manta::proto::ScannerAction::kNewContact:
      variant_name = kScannerFeatureTimerExecutePopulatedNewContactAction;
      break;
    case manta::proto::ScannerAction::kNewGoogleDoc:
      variant_name = kScannerFeatureTimerExecutePopulatedNewGoogleDocAction;
      break;
    case manta::proto::ScannerAction::kNewGoogleSheet:
      variant_name = kScannerFeatureTimerExecutePopulatedNewGoogleSheetAction;
      break;
    case manta::proto::ScannerAction::kCopyToClipboard:
      variant_name =
          kScannerFeatureTimerExecutePopulatedNewCopyToClipboardAction;
      break;
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      break;
  }
  if (variant_name.empty()) {
    return;
  }
  base::UmaHistogramMediumTimes(variant_name,
                                base::TimeTicks::Now() - execute_start_time);
}

void RecordPopulateActionTimer(
    manta::proto::ScannerAction::ActionCase action_case,
    base::TimeTicks request_start_time) {
  // TODO(b/363101363): Add tests.
  std::string_view variant_name;
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      variant_name = kScannerFeatureTimerPopulateNewCalendarEventAction;
      break;
    case manta::proto::ScannerAction::kNewContact:
      variant_name = kScannerFeatureTimerPopulateNewContactAction;
      break;
    case manta::proto::ScannerAction::kNewGoogleDoc:
      variant_name = kScannerFeatureTimerPopulateNewGoogleDocAction;
      break;
    case manta::proto::ScannerAction::kNewGoogleSheet:
      variant_name = kScannerFeatureTimerPopulateNewGoogleSheetAction;
      break;
    case manta::proto::ScannerAction::kCopyToClipboard:
      variant_name = kScannerFeatureTimerPopulateNewCopyToClipboardAction;
      break;
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      break;
  }
  if (variant_name.empty()) {
    return;
  }
  base::UmaHistogramMediumTimes(variant_name,
                                base::TimeTicks::Now() - request_start_time);
}

void RecordPopulateActionFailure(
    manta::proto::ScannerAction::ActionCase action_case) {
  // TODO(b/363101363): Add tests.
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      RecordScannerFeatureUserState(kNewCalendarEventActionPopulationFailed);
      return;
    case manta::proto::ScannerAction::kNewContact:
      RecordScannerFeatureUserState(kNewContactActionPopulationFailed);
      return;
    case manta::proto::ScannerAction::kNewGoogleDoc:
      RecordScannerFeatureUserState(kNewGoogleDocActionPopulationFailed);
      return;
    case manta::proto::ScannerAction::kNewGoogleSheet:
      RecordScannerFeatureUserState(kNewGoogleSheetActionPopulationFailed);
      return;
    case manta::proto::ScannerAction::kCopyToClipboard:
      RecordScannerFeatureUserState(kCopyToClipboardActionPopulationFailed);
      return;
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      return;
  }
}

void RecordActionExecutionAndRun(
    manta::proto::ScannerAction::ActionCase action_case,
    base::TimeTicks execute_start_time,
    ScannerCommandCallback action_finished_callback,
    bool success) {
  // TODO(b/363101363): Add tests.
  switch (action_case) {
    case manta::proto::ScannerAction::kNewEvent:
      RecordScannerFeatureUserState(
          success ? kNewCalendarEventActionFinishedSuccessfully
                  : kNewCalendarEventPopulatedActionExecutionFailed);
      break;
    case manta::proto::ScannerAction::kNewContact:
      RecordScannerFeatureUserState(
          success ? kNewContactActionFinishedSuccessfully
                  : kNewContactPopulatedActionExecutionFailed);
      break;
    case manta::proto::ScannerAction::kNewGoogleDoc:
      RecordScannerFeatureUserState(
          success ? kNewGoogleDocActionFinishedSuccessfully
                  : kNewGoogleDocPopulatedActionExecutionFailed);
      break;
    case manta::proto::ScannerAction::kNewGoogleSheet:
      RecordScannerFeatureUserState(
          success ? kNewGoogleSheetActionFinishedSuccessfully
                  : kNewGoogleSheetPopulatedActionExecutionFailed);
      break;
    case manta::proto::ScannerAction::kCopyToClipboard:
      RecordScannerFeatureUserState(
          success ? kCopyToClipboardActionFinishedSuccessfully
                  : kCopyToClipboardPopulatedActionExecutionFailed);
      break;
    case manta::proto::ScannerAction::ACTION_NOT_SET:
      break;
  }
  RecordExecutePopulatedActionTimer(action_case, execute_start_time);
  std::move(action_finished_callback).Run(success);
}

// Executes the populated action, if it exists, calling
// `action_finished_callback` with the result of the execution.
void ExecutePopulatedAction(
    manta::proto::ScannerAction::ActionCase action_case,
    base::TimeTicks request_start_time,
    base::WeakPtr<ScannerCommandDelegate> delegate,
    base::OnceCallback<void(manta::proto::ScannerAction populated_action,
                            bool success)> action_finished_callback,
    manta::proto::ScannerAction populated_action) {
  RecordPopulateActionTimer(action_case, request_start_time);
  if (populated_action.action_case() ==
      manta::proto::ScannerAction::ACTION_NOT_SET) {
    RecordPopulateActionFailure(action_case);
    std::move(action_finished_callback).Run(std::move(populated_action), false);
    return;
  }

  ScannerCommandCallback record_metrics_callback = base::BindOnce(
      &RecordActionExecutionAndRun, action_case, base::TimeTicks::Now(),
      base::BindOnce(std::move(action_finished_callback), populated_action));

  HandleScannerCommand(std::move(delegate),
                       ScannerActionToCommand(std::move(populated_action)),
                       std::move(record_metrics_callback));
}

void OnFeedbackFormSendButtonClicked(const AccountId& account_id,
                                     base::Value::Dict action_dict,
                                     ScannerFeedbackInfo feedback_info,
                                     const std::string& user_description) {
  RecordScannerFeatureUserState(ScannerFeatureUserState::kFeedbackSent);
  std::optional<std::string> pretty_printed_action = base::WriteJsonWithOptions(
      action_dict, base::JsonOptions::OPTIONS_PRETTY_PRINT);
  // JSON serialisation should always succeed as the depth of the Dict is fixed,
  // and no binary values should appear in the Dict.
  CHECK(pretty_printed_action.has_value());

  // Work around limitations with `feedback::RedactionTool` by prepending two
  // spaces and appending a new line to any data to be redacted.
  std::string description =
      base::StrCat({"details:  ", *pretty_printed_action,
                    "\nuser_description:  ", user_description, "\n"});

  Shell::Get()->shell_delegate()->SendSpecializedFeatureFeedback(
      account_id, feedback::kScannerFeedbackProductId, std::move(description),
      std::string(base::as_string_view(*feedback_info.screenshot)),
      /*image_mime_type=*/"image/jpeg");
}

void SetStringIfPresent(const base::Value::Dict* dict,
                        const std::string& key,
                        auto* field) {
  if (const std::string* value = dict->FindString(key)) {
    *field = *value;
  }
}

manta::proto::ScannerAction ScannerActionFromValue(
    const base::Value::Dict& dict) {
  manta::proto::ScannerAction action;

  // The input dictionary dict is expected to contain exactly one of the
  // following top-level keys, representing the type of action to perform.
  if (const base::Value::Dict* new_event = dict.FindDict("new_event")) {
    auto* event = action.mutable_new_event();
    SetStringIfPresent(new_event, "title", event->mutable_title());
    SetStringIfPresent(new_event, "dates", event->mutable_dates());
    SetStringIfPresent(new_event, "description", event->mutable_description());
    SetStringIfPresent(new_event, "location", event->mutable_location());
  } else if (const base::Value::Dict* copy_action =
                 dict.FindDict("copy_to_clipboard")) {
    auto* clipboard = action.mutable_copy_to_clipboard();
    SetStringIfPresent(copy_action, "plain_text",
                       clipboard->mutable_plain_text());
    SetStringIfPresent(copy_action, "html_text",
                       clipboard->mutable_html_text());
  } else {
    LOG(ERROR) << "Unknown scanner action type in mock response: " << dict;
  }

  return action;
}

std::unique_ptr<manta::proto::ScannerOutput> CreateMockScannerOutput(
    const std::vector<std::string> mock_responses) {
  auto mock_output = std::make_unique<manta::proto::ScannerOutput>();
  manta::proto::ScannerObject* object = mock_output->add_objects();

  for (const std::string& json_string : mock_responses) {
    std::optional<base::Value> parsed_json =
        base::JSONReader::Read(json_string, base::JSON_ALLOW_TRAILING_COMMAS);

    if (!parsed_json.has_value() || !parsed_json->is_dict()) {
      LOG(ERROR) << "Invalid json string: " << json_string;
      continue;
    }

    base::Value::Dict& action_dict = parsed_json->GetDict();
    manta::proto::ScannerAction action = ScannerActionFromValue(action_dict);
    if (action.action_case() != manta::proto::ScannerAction::ACTION_NOT_SET) {
      *object->add_actions() = std::move(action);
    }
  }
  return mock_output;
}
}  // namespace

ScannerController::ScannerController(
    std::unique_ptr<ScannerDelegate> delegate,
    SessionControllerImpl& session_controller,
    const ScreenPinningController* screen_pinning_controller)
    : delegate_(std::move(delegate)),
      session_controller_(session_controller),
      screen_pinning_controller_(screen_pinning_controller) {
  if (screen_pinning_controller_ == nullptr) {
    CHECK_IS_TEST();
  }
}

ScannerController::~ScannerController() = default;

// static
void ScannerController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(prefs::kScannerEnabled, true);
  registry->RegisterIntegerPref(
      prefs::kScannerEnterprisePolicyAllowed,
      static_cast<int>(ScannerEnterprisePolicy::kAllowedWithModelImprovement));

  registry->RegisterBooleanPref(
      prefs::kScannerEntryPointDisclaimerAckSmartActionsButton, false);
  registry->RegisterBooleanPref(
      prefs::kScannerEntryPointDisclaimerAckSunfishSession, false);
}

// static
bool ScannerController::CanShowUiForShell() {
  if (!Shell::HasInstance()) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalseDueToNoShellInstance);
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalse);
    return false;
  }

  ScannerController* controller = Shell::Get()->scanner_controller();
  if (!controller) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::
            kCanShowUiReturnedFalseDueToNoControllerOnShell);
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalse);
    return false;
  }

  return controller->CanShowUi();
}

void ScannerController::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  scanner_session_ = nullptr;
  command_delegate_ = nullptr;
}

bool ScannerController::CanShowUi() {
  if (screen_pinning_controller_ == nullptr) {
    CHECK_IS_TEST();
  } else if (screen_pinning_controller_->IsPinned()) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalseDueToPinnedMode);
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalse);
    return false;
  }

  // Check enterprise policy.
  const AccountId& account_id = session_controller_->GetActiveAccountId();
  PrefService* prefs =
      session_controller_->GetUserPrefServiceForUser(account_id);
  // We assume a default value of 1 (allowed without model improvement) if the
  // value is invalid, or the pref service isn't valid.
  if (prefs != nullptr &&
      prefs->GetInteger(prefs::kScannerEnterprisePolicyAllowed) ==
          static_cast<int>(ScannerEnterprisePolicy::kDisallowed)) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalseDueToEnterprisePolicy);
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalse);
    return false;
  }

  ScannerProfileScopedDelegate* profile_scoped_delegate =
      delegate_->GetProfileScopedDelegate();

  if (profile_scoped_delegate == nullptr) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::
            kCanShowUiReturnedFalseDueToNoProfileScopedDelegate);
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalse);
    return false;
  }

  specialized_features::FeatureAccessFailureSet checks =
      profile_scoped_delegate->CheckFeatureAccess();

  bool consent_accepted = true;
  bool show_ui = true;

  for (specialized_features::FeatureAccessFailure failure : checks) {
    switch (failure) {
      case specialized_features::FeatureAccessFailure::kConsentNotAccepted:
        consent_accepted = false;
        break;

      case specialized_features::FeatureAccessFailure::kDisabledInSettings:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::
                kCanShowUiReturnedFalseDueToSettingsToggle);
        show_ui = false;
        break;

      case specialized_features::FeatureAccessFailure::kFeatureFlagDisabled:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::kCanShowUiReturnedFalseDueToFeatureFlag);
        show_ui = false;
        break;

      case specialized_features::FeatureAccessFailure::
          kFeatureManagementCheckFailed:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::
                kCanShowUiReturnedFalseDueToFeatureManagement);
        show_ui = false;
        break;

      case specialized_features::FeatureAccessFailure::kSecretKeyCheckFailed:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::kCanShowUiReturnedFalseDueToSecretKey);
        show_ui = false;
        break;

      case specialized_features::FeatureAccessFailure::
          kAccountCapabilitiesCheckFailed:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::
                kCanShowUiReturnedFalseDueToAccountCapabilities);
        show_ui = false;
        break;

      case specialized_features::FeatureAccessFailure::kCountryCheckFailed:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::kCanShowUiReturnedFalseDueToCountry);
        show_ui = false;
        break;

      case specialized_features::FeatureAccessFailure::
          kDisabledInKioskModeCheckFailed:
        RecordScannerFeatureUserState(
            ScannerFeatureUserState::kCanShowUiReturnedFalseDueToKioskMode);
        show_ui = false;
        break;
    }
  }

  if (!show_ui) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedFalse);
    return false;
  }

  if (!consent_accepted) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedTrueWithoutConsent);
  } else {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::kCanShowUiReturnedTrueWithConsent);
  }
  return true;
}

bool ScannerController::CanShowFeatureSettingsToggle() {
  if (screen_pinning_controller_ == nullptr) {
    CHECK_IS_TEST();
  } else if (screen_pinning_controller_->IsPinned()) {
    return false;
  }
  // Intentionally ignore enterprise policy here, as we still want to show the
  // settings toggle (as disabled).

  ScannerProfileScopedDelegate* profile_scoped_delegate =
      delegate_->GetProfileScopedDelegate();

  if (profile_scoped_delegate == nullptr) {
    return false;
  }

  specialized_features::FeatureAccessFailureSet checks =
      profile_scoped_delegate->CheckFeatureAccess();

  // Show settings toggle even if the setting is disabled or consent not
  // accepted.
  // Hence we ignore these checks if they have failed.
  checks.Remove(
      specialized_features::FeatureAccessFailure::kDisabledInSettings);
  checks.Remove(
      specialized_features::FeatureAccessFailure::kConsentNotAccepted);
  return checks.empty();
}

bool ScannerController::CanStartSession() {
  if (screen_pinning_controller_ == nullptr) {
    CHECK_IS_TEST();
  } else if (screen_pinning_controller_->IsPinned()) {
    return false;
  }
  // Check enterprise policy.
  const AccountId& account_id = session_controller_->GetActiveAccountId();
  PrefService* prefs =
      session_controller_->GetUserPrefServiceForUser(account_id);
  // We assume a default value of 1 (allowed without model improvement) if the
  // value is invalid, or the pref service isn't valid.
  if (prefs != nullptr &&
      prefs->GetInteger(prefs::kScannerEnterprisePolicyAllowed) ==
          static_cast<int>(ScannerEnterprisePolicy::kDisallowed)) {
    return false;
  }

  ScannerProfileScopedDelegate* profile_scoped_delegate =
      delegate_->GetProfileScopedDelegate();

  if (profile_scoped_delegate == nullptr) {
    return false;
  }

  if (!profile_scoped_delegate->CheckFeatureAccess().empty()) {
    return false;
  }

  return true;
}

ScannerSession* ScannerController::StartNewSession() {
  // Reset the current session if there is one. We do this here to ensure that
  // the old session is destroyed before attempting to create the new session
  // (to avoid subtle issues from having simultaneously existing sessions).
  scanner_session_ = nullptr;
  if (CanStartSession()) {
    scanner_session_ =
        std::make_unique<ScannerSession>(delegate_->GetProfileScopedDelegate());
  }
  return scanner_session_.get();
}

bool ScannerController::FetchActionsForImage(
    scoped_refptr<base::RefCountedMemory> jpeg_bytes,
    ScannerSession::FetchActionsCallback callback) {
  if (!scanner_session_) {
    std::move(callback).Run({});
    return false;
  }

  if (!mock_scanner_responses_for_testing_.empty()) {
    scanner_session_->SetMockScannerOutput(CreateMockScannerOutput(
        std::move(mock_scanner_responses_for_testing_)));
    mock_scanner_responses_for_testing_.clear();
  }

  scanner_session_->FetchActionsForImage(jpeg_bytes, std::move(callback));
  return true;
}

void ScannerController::OnSessionUIClosed() {
  scanner_session_ = nullptr;
}

void ScannerController::ExecuteAction(
    const ScannerActionViewModel& scanner_action) {
  if (!scanner_session_) {
    return;
  }

  if (!mock_scanner_responses_for_testing_.empty()) {
    scanner_session_->SetMockScannerOutput(CreateMockScannerOutput(
        std::move(mock_scanner_responses_for_testing_)));
    mock_scanner_responses_for_testing_.clear();
  }

  // Keep the existing `command_delegate_` if there is one, to allow commands
  // from previous sessions to continue in the background if needed.
  if (!command_delegate_) {
    command_delegate_ = std::make_unique<ScannerCommandDelegateImpl>(
        delegate_->GetProfileScopedDelegate());
  }
  const manta::proto::ScannerAction::ActionCase action_case =
      scanner_action.GetActionCase();
  scanner_session_->PopulateAction(
      scanner_action.downscaled_jpeg_bytes(),
      scanner_action.unpopulated_action(),
      base::BindOnce(&ExecutePopulatedAction, action_case,
                     base::TimeTicks::Now(), command_delegate_->GetWeakPtr(),
                     base::BindOnce(&ScannerController::OnActionFinished,
                                    weak_ptr_factory_.GetWeakPtr(), action_case,
                                    scanner_action.downscaled_jpeg_bytes())));
  ShowActionProgressNotification(scanner_action);
}

void ScannerController::OpenFeedbackDialog(
    const AccountId& account_id,
    manta::proto::ScannerAction action,
    scoped_refptr<base::RefCountedMemory> screenshot) {
  RecordScannerFeatureUserState(ScannerFeatureUserState::kFeedbackFormOpened);
  base::Value::Dict action_dict = ScannerActionToDict(std::move(action));

  std::optional<std::string> user_facing_string = ValueToUserFacingString(
      action_dict, kUserFacingStringDepthLimit, kUserFacingStringOutputLimit);
  // `user_facing_string` can only be nullopt if:
  // - `ScannerActionToDict` output a binary value, which is impossible,
  // - `ScannerActionToDict` output a more-than-twenty nested value, which is
  //   impossible (all returned values are at most three-nested)
  // - the excessively large output limit is hit, which should be impossible.
  CHECK(user_facing_string.has_value());

  delegate_->OpenFeedbackDialog(
      account_id,
      ScannerFeedbackInfo(std::move(*user_facing_string),
                          std::move(screenshot)),
      base::BindOnce(&OnFeedbackFormSendButtonClicked, account_id,
                     std::move(action_dict)));
}

void ScannerController::SetOnActionFinishedForTesting(
    OnActionFinishedCallback callback) {
  on_action_finished_for_testing_ = std::move(callback);
}

bool ScannerController::HasActiveSessionForTesting() const {
  return !!scanner_session_;
}

void ScannerController::OnActionFinished(
    manta::proto::ScannerAction::ActionCase action_case,
    scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes,
    manta::proto::ScannerAction populated_action,
    bool success) {
  // Remove the action progress notification.
  message_center::MessageCenter::Get()->RemoveNotification(
      kScannerActionNotificationId,
      /*by_user=*/false);

  if (success) {
    if (features::IsScannerFeedbackToastEnabled()) {
      ToastData toast_data(kScannerActionSuccessToastId,
                           ToastCatalogName::kScannerActionSuccess,
                           GetToastMessageForActionSuccess(action_case));

      // TODO: crbug.com/367882164 - Pass in the account ID to this method to
      // ensure that the feedback form is shown for the same account that
      // performed the action.
      const AccountId& account_id = session_controller_->GetActiveAccountId();
      PrefService* prefs =
          session_controller_->GetUserPrefServiceForUser(account_id);

      if (prefs &&
          prefs->GetInteger(prefs::kScannerEnterprisePolicyAllowed) ==
              static_cast<int>(
                  ScannerEnterprisePolicy::kAllowedWithModelImprovement)) {
        toast_data.button_type = ToastData::ButtonType::kIconButton;
        toast_data.button_text = l10n_util::GetStringUTF16(
            IDS_ASH_SCANNER_ACTION_TOAST_FEEDBACK_ICON_ACCESSIBLE_NAME);
        toast_data.button_icon = &kFeedbackIcon;
        // TODO: crbug.com/259100049 - Change this to be `BindOnce` once
        // `ToastData::button_callback` is migrated to be a `OnceClosure`.
        toast_data.button_callback = base::BindRepeating(
            &ScannerController::OpenFeedbackDialog,
            weak_ptr_factory_.GetWeakPtr(), account_id,
            std::move(populated_action), std::move(downscaled_jpeg_bytes));
      }

      ToastManager::Get()->Show(std::move(toast_data));
    } else if (action_case == manta::proto::ScannerAction::kCopyToClipboard) {
      // If feedback is disabled, only show a success toast for the copy to
      // clipboard action.
      ToastData toast_data(kScannerActionSuccessToastId,
                           ToastCatalogName::kScannerActionSuccess,
                           GetToastMessageForActionSuccess(action_case));
      ToastManager::Get()->Show(std::move(toast_data));
    }
  } else {
    ToastManager::Get()->Show(ToastData(
        kScannerActionFailureToastId, ToastCatalogName::kScannerActionFailure,
        GetToastMessageForActionFailure(action_case)));
  }

  if (!on_action_finished_for_testing_.is_null()) {
    CHECK_IS_TEST();
    std::move(on_action_finished_for_testing_).Run(success);
  }
}

void ScannerController::SetScannerResponsesForTesting(
    std::vector<std::string> responses) {
  mock_scanner_responses_for_testing_ = std::move(responses);
}

}  // namespace ash