chromium/ash/picker/picker_controller.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 "ash/picker/picker_controller.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/picker/model/picker_action_type.h"
#include "ash/picker/model/picker_caps_lock_position.h"
#include "ash/picker/model/picker_emoji_history_model.h"
#include "ash/picker/model/picker_emoji_suggester.h"
#include "ash/picker/model/picker_mode_type.h"
#include "ash/picker/model/picker_model.h"
#include "ash/picker/model/picker_search_results_section.h"
#include "ash/picker/picker_asset_fetcher.h"
#include "ash/picker/picker_asset_fetcher_impl.h"
#include "ash/picker/picker_caps_lock_bubble_controller.h"
#include "ash/picker/picker_copy_media.h"
#include "ash/picker/picker_insert_media_request.h"
#include "ash/picker/picker_paste_request.h"
#include "ash/picker/picker_rich_media.h"
#include "ash/picker/picker_suggestions_controller.h"
#include "ash/picker/picker_transform_case.h"
#include "ash/picker/search/picker_search_controller.h"
#include "ash/picker/views/picker_caps_lock_state_view.h"
#include "ash/picker/views/picker_icons.h"
#include "ash/picker/views/picker_positioning.h"
#include "ash/picker/views/picker_view.h"
#include "ash/picker/views/picker_view_delegate.h"
#include "ash/picker/views/picker_widget.h"
#include "ash/public/cpp/clipboard_history_controller.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/picker/picker_client.h"
#include "ash/public/cpp/picker/picker_search_result.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/hash/sha1.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/aura/client/focus_client.h"
#include "ui/aura/window.h"
#include "ui/base/emoji/emoji_panel_helper.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/ime_keyboard.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/base/ime/ash/text_input_target.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/screen.h"
#include "ui/events/ash/keyboard_capability.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"

namespace ash {

namespace {

bool g_should_check_key = true;
bool g_feature_tour_enabled = true;

// The hash value for the feature key of the Picker feature, used for
// development.
constexpr std::string_view kPickerFeatureDevKeyHash(
    "\xE1\xC0\x09\x7F\xBE\x03\xBF\x48\xA7\xA0\x30\x53\x07\x4F\xFB\xC5\x6D\xD4"
    "\x22\x5F",
    base::kSHA1Length);

// The hash value for the feature key of the Picker feature, used in some tests.
constexpr std::string_view kPickerFeatureTestKeyHash(
    "\xE7\x2C\x99\xD7\x99\x89\xDB\xA5\x9D\x06\x4A\xED\xDF\xE5\x30\xA7\x8C\x76"
    "\x00\x89",
    base::kSHA1Length);

enum class PickerFeatureKeyType { kNone, kDev, kTest };

// When spoken feedback is enabled, closing the widget after an insert is
// delayed by this amount.
constexpr base::TimeDelta kCloseWidgetDelay = base::Milliseconds(200);

constexpr int kCapsLockMinimumTopDisplayCount = 5;
constexpr float kCapsLockRatioThresholdForTop = 0.8;
constexpr float kCapsLockRatioThresholdForBottom = 0.2;

constexpr std::string_view kSupportUrl =
    "https://support.google.com/chromebook?p=dugong";

PickerFeatureKeyType MatchPickerFeatureKeyHash() {
  static const PickerFeatureKeyType key_type = []() {
    // Command line looks like:
    //  out/Default/chrome --user-data-dir=/tmp/tmp123
    //  --picker-feature-key="INSERT KEY HERE" --enable-features=PickerFeature
    const std::string provided_key_hash = base::SHA1HashString(
        base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
            switches::kPickerFeatureKey));
    if (provided_key_hash == kPickerFeatureDevKeyHash) {
      return PickerFeatureKeyType::kDev;
    }
    if (provided_key_hash == kPickerFeatureTestKeyHash) {
      return PickerFeatureKeyType::kTest;
    }
    return PickerFeatureKeyType::kNone;
  }();

  return key_type;
}

ui::TextInputClient* GetFocusedTextInputClient() {
  const ui::InputMethod* input_method =
      IMEBridge::Get()->GetInputContextHandler()->GetInputMethod();
  if (!input_method || !input_method->GetTextInputClient()) {
    return nullptr;
  }
  return input_method->GetTextInputClient();
}

// Gets the current caret bounds in universal screen coordinates in DIP. Returns
// an empty rect if there is no active caret or the caret bounds can't be
// determined (e.g. no focused input field).
gfx::Rect GetCaretBounds() {
  if (ui::TextInputClient* client = GetFocusedTextInputClient()) {
    return client->GetCaretBounds();
  }
  return gfx::Rect();
}

// Gets the current cursor point in universal screen coordinates in DIP.
gfx::Point GetCursorPoint() {
  return display::Screen::GetScreen()->GetCursorScreenPoint();
}

// Gets the bounds of the current focused window in universal screen coordinates
// in DIP. Returns an empty rect if there is no currently focused window.
gfx::Rect GetFocusedWindowBounds() {
  return window_util::GetFocusedWindow()
             ? window_util::GetFocusedWindow()->GetBoundsInScreen()
             : gfx::Rect();
}

input_method::ImeKeyboard& GetImeKeyboard() {
  auto* input_method_manager = input_method::InputMethodManager::Get();
  CHECK(input_method_manager);
  input_method::ImeKeyboard* ime_keyboard =
      input_method_manager->GetImeKeyboard();
  CHECK(ime_keyboard);
  return *ime_keyboard;
}

// The user can ask to insert rich media, a clipboard item, or insert nothing.
using InsertionContent =
    std::variant<PickerRichMedia, PickerClipboardResult, std::monostate>;

InsertionContent GetInsertionContentForResult(
    const PickerSearchResult& result) {
  using ReturnType = InsertionContent;
  return std::visit(
      base::Overloaded{
          [](const PickerTextResult& data) -> ReturnType {
            return PickerTextMedia(data.primary_text);
          },
          [](const PickerEmojiResult& data) -> ReturnType {
            return PickerTextMedia(data.text);
          },
          [](const PickerClipboardResult& data) -> ReturnType { return data; },
          [](const PickerBrowsingHistoryResult& data) -> ReturnType {
            return PickerLinkMedia(data.url, base::UTF16ToUTF8(data.title));
          },
          [](const PickerLocalFileResult& data) -> ReturnType {
            return PickerLocalFileMedia(data.file_path);
          },
          [](const PickerDriveFileResult& data) -> ReturnType {
            return PickerLinkMedia(data.url, base::UTF16ToUTF8(data.title));
          },
          [](const PickerCategoryResult& data) -> ReturnType {
            return std::monostate();
          },
          [](const PickerSearchRequestResult& data) -> ReturnType {
            return std::monostate();
          },
          [](const PickerEditorResult& data) -> ReturnType {
            return std::monostate();
          },
          [](const PickerNewWindowResult& data) -> ReturnType {
            return std::monostate();
          },
          [](const PickerCapsLockResult& data) -> ReturnType {
            return std::monostate();
          },
          [](const PickerCaseTransformResult& data) -> ReturnType {
            return std::monostate();
          },
      },
      result);
}

std::vector<PickerSearchResultsSection> CreateSingleSectionForCategoryResults(
    PickerSectionType section_type,
    std::vector<PickerSearchResult> results) {
  if (results.empty()) {
    return {};
  }
  return {PickerSearchResultsSection(section_type, std::move(results),
                                     /*has_more_results=*/false)};
}

std::u16string TransformText(std::u16string_view text,
                             PickerCaseTransformResult::Type type) {
  switch (type) {
    case PickerCaseTransformResult::Type::kUpperCase:
      return PickerTransformToUpperCase(text);
    case PickerCaseTransformResult::Type::kLowerCase:
      return PickerTransformToLowerCase(text);
    case PickerCaseTransformResult::Type::kTitleCase:
      return PickerTransformToTitleCase(text);
  }
  NOTREACHED();
}

void OpenLink(const GURL& url) {
  ash::NewWindowDelegate::GetPrimary()->OpenUrl(
      url, ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      ash::NewWindowDelegate::Disposition::kNewForegroundTab);
}

void OpenFile(const base::FilePath& path) {
  ash::NewWindowDelegate::GetPrimary()->OpenFile(path);
}

GURL GetUrlForNewWindow(PickerNewWindowResult::Type type) {
  switch (type) {
    case PickerNewWindowResult::Type::kDoc:
      return GURL("https://docs.new");
    case PickerNewWindowResult::Type::kSheet:
      return GURL("https://sheets.new");
    case PickerNewWindowResult::Type::kSlide:
      return GURL("https://slides.new");
    case PickerNewWindowResult::Type::kChrome:
      return GURL("chrome://newtab");
  }
}

ui::EmojiPickerCategory EmojiResultTypeToCategory(
    PickerEmojiResult::Type type) {
  switch (type) {
    case PickerEmojiResult::Type::kEmoji:
      return ui::EmojiPickerCategory::kEmojis;
    case PickerEmojiResult::Type::kSymbol:
      return ui::EmojiPickerCategory::kSymbols;
    case PickerEmojiResult::Type::kEmoticon:
      return ui::EmojiPickerCategory::kEmoticons;
  }
}

}  // namespace

PickerController::PickerController()
    : caps_lock_bubble_controller_(&GetImeKeyboard()),
      asset_fetcher_(std::make_unique<PickerAssetFetcherImpl>(this)) {}

PickerController::~PickerController() {
  // `widget_` depends on `this`. Destroy the widget synchronously to avoid a
  // dangling pointer.
  if (widget_) {
    widget_->CloseNow();
  }
}

bool PickerController::IsFeatureEnabled() {
  if (!features::IsPickerUpdateEnabled()) {
    return false;
  }

  if (!g_should_check_key) {
    return true;
  }

  if (base::FeatureList::IsEnabled(ash::features::kPickerDogfood) &&
      client_->IsFeatureAllowedForDogfood()) {
    return true;
  }

  if (MatchPickerFeatureKeyHash() == PickerFeatureKeyType::kNone) {
    LOG(ERROR) << "Provided feature key does not match with the expected one.";
    return false;
  }

  return true;
}

void PickerController::DisableFeatureKeyCheck() {
  g_should_check_key = false;
}

void PickerController::DisableFeatureTourForTesting() {
  CHECK_IS_TEST();
  g_feature_tour_enabled = false;
}

void PickerController::SetClient(PickerClient* client) {
  client_ = client;
  // The destructor of `PickerSearchRequest` inside `PickerSearchController` may
  // result in "stop search" calls to the PREVIOUS `PickerClient`.
  if (client_ == nullptr) {
    suggestions_controller_ = nullptr;
    search_controller_ = nullptr;
  } else {
    suggestions_controller_ =
        std::make_unique<PickerSuggestionsController>(client_);
    search_controller_ =
        std::make_unique<PickerSearchController>(client_, kBurnInPeriod);
  }
}

void PickerController::OnClientProfileSet() {
  if (client_ == nullptr || search_controller_ == nullptr) {
    return;
  }

  search_controller_->LoadEmojiLanguagesFromPrefs();
}

void PickerController::ToggleWidget(
    const base::TimeTicks trigger_event_timestamp) {
  if (!IsFeatureEnabled()) {
    return;
  }

  // Show the feature tour if it's the first time this feature is used.
  if (PrefService* prefs = GetPrefs();
      g_feature_tour_enabled && prefs &&
      feature_tour_.MaybeShowForFirstUse(
          prefs,
          client_->IsEligibleForEditor()
              ? PickerFeatureTour::EditorStatus::kEligible
              : PickerFeatureTour::EditorStatus::kNotEligible,
          base::BindRepeating(OpenLink, GURL(kSupportUrl)),
          base::BindRepeating(&PickerController::ShowWidgetPostFeatureTour,
                              weak_ptr_factory_.GetWeakPtr()))) {
    return;
  }

  if (widget_) {
    CloseWidget();
  } else {
    ShowWidget(trigger_event_timestamp, WidgetTriggerSource::kDefault);
  }
}

std::vector<PickerCategory> PickerController::GetAvailableCategories() {
  return session_ == nullptr ? std::vector<PickerCategory>{}
                             : session_->model.GetAvailableCategories();
}

void PickerController::GetZeroStateSuggestedResults(
    SuggestedResultsCallback callback) {
  suggestions_controller_->GetSuggestions(session_->model, std::move(callback));
}

void PickerController::GetResultsForCategory(PickerCategory category,
                                             SearchResultsCallback callback) {
  const PickerSectionType section_type =
      (category == PickerCategory::kUnitsMaths ||
       category == PickerCategory::kDatesTimes)
          ? PickerSectionType::kExamples
          : PickerSectionType::kNone;

  suggestions_controller_->GetSuggestionsForCategory(
      category,
      base::BindRepeating(CreateSingleSectionForCategoryResults, section_type)
          .Then(std::move(callback)));
}

void PickerController::StartSearch(std::u16string_view query,
                                   std::optional<PickerCategory> category,
                                   SearchResultsCallback callback) {
  CHECK(search_controller_);
  CHECK(session_);
  search_controller_->StartSearch(
      query, std::move(category),
      {
          .available_categories = GetAvailableCategories(),
          .caps_lock_state_to_search = !session_->model.is_caps_lock_enabled(),
          .search_case_transforms =
              session_->model.GetMode() == PickerModeType::kHasSelection,
      },
      std::move(callback));
}

void PickerController::StopSearch() {
  CHECK(search_controller_);
  search_controller_->StopSearch();
}

void PickerController::StartEmojiSearch(std::u16string_view query,
                                        EmojiSearchResultsCallback callback) {
  search_controller_->StartEmojiSearch(query, std::move(callback));
}

void PickerController::CloseWidgetThenInsertResultOnNextFocus(
    const PickerSearchResult& result) {
  InsertResultOnNextFocus(result);

  client_->Announce(
      l10n_util::GetStringUTF16(IDS_PICKER_INSERTION_ANNOUNCEMENT_TEXT));

  if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
    close_widget_delay_timer_.Start(
        FROM_HERE, kCloseWidgetDelay,
        base::BindOnce(&PickerController::CloseWidget,
                       weak_ptr_factory_.GetWeakPtr()));
  } else {
    CloseWidget();
  }
}

void PickerController::OpenResult(const PickerSearchResult& result) {
  return std::visit(
      base::Overloaded{
          [](const PickerTextResult& data) { NOTREACHED(); },
          [](const PickerEmojiResult& data) { NOTREACHED(); },
          [](const PickerClipboardResult& data) { NOTREACHED(); },
          [&](const PickerBrowsingHistoryResult& data) {
            session_->session_metrics.SetOutcome(
                PickerSessionMetrics::SessionOutcome::kOpenLink);
            OpenLink(data.url);
          },
          [&](const PickerLocalFileResult& data) {
            session_->session_metrics.SetOutcome(
                PickerSessionMetrics::SessionOutcome::kOpenFile);
            OpenFile(data.file_path);
          },
          [&](const PickerDriveFileResult& data) {
            session_->session_metrics.SetOutcome(
                PickerSessionMetrics::SessionOutcome::kOpenLink);
            OpenLink(data.url);
          },
          [](const PickerCategoryResult& data) { NOTREACHED(); },
          [](const PickerSearchRequestResult& data) { NOTREACHED(); },
          [](const PickerEditorResult& data) { NOTREACHED(); },
          [&](const PickerNewWindowResult& data) {
            session_->session_metrics.SetOutcome(
                PickerSessionMetrics::SessionOutcome::kCreate);
            OpenLink(GetUrlForNewWindow(data.type));
          },
          [&](const PickerCapsLockResult& data) {
            session_->session_metrics.SetOutcome(
                PickerSessionMetrics::SessionOutcome::kFormat);
            GetImeKeyboard().SetCapsLockEnabled(data.enabled);
          },
          [&](const PickerCaseTransformResult& data) {
            if (!session_) {
              return;
            }
            session_->session_metrics.SetOutcome(
                PickerSessionMetrics::SessionOutcome::kFormat);
            std::u16string_view selected_text = session_->model.selected_text();
            InsertResultOnNextFocus(
                PickerTextResult(TransformText(selected_text, data.type),
                                 PickerTextResult::Source::kCaseTransform));
          },
      },
      result);
}

void PickerController::ShowEmojiPicker(ui::EmojiPickerCategory category,
                                       std::u16string_view query) {
  ui::ShowEmojiPanelInSpecificMode(category,
                                   ui::EmojiPickerFocusBehavior::kAlwaysShow,
                                   base::UTF16ToUTF8(query));
}

void PickerController::ShowEditor(std::optional<std::string> preset_query_id,
                                  std::optional<std::string> freeform_text) {
  if (!show_editor_callback_.is_null()) {
    std::move(show_editor_callback_)
        .Run(std::move(preset_query_id), std::move(freeform_text));
  }
}

PickerAssetFetcher* PickerController::GetAssetFetcher() {
  return asset_fetcher_.get();
}

PickerSessionMetrics& PickerController::GetSessionMetrics() {
  return session_->session_metrics;
}

PickerActionType PickerController::GetActionForResult(
    const PickerSearchResult& result) {
  CHECK(session_);
  const PickerModeType mode = session_->model.GetMode();
  return std::visit(
      base::Overloaded{
          [mode](const PickerTextResult& data) {
            CHECK(mode == PickerModeType::kNoSelection ||
                  mode == PickerModeType::kHasSelection);
            return PickerActionType::kInsert;
          },
          [mode](const PickerEmojiResult& data) {
            CHECK(mode == PickerModeType::kNoSelection ||
                  mode == PickerModeType::kHasSelection);
            return PickerActionType::kInsert;
          },
          [mode](const PickerClipboardResult& data) {
            CHECK(mode == PickerModeType::kNoSelection ||
                  mode == PickerModeType::kHasSelection);
            return PickerActionType::kInsert;
          },
          [mode](const PickerBrowsingHistoryResult& data) {
            return mode == PickerModeType::kUnfocused
                       ? PickerActionType::kOpen
                       : PickerActionType::kInsert;
          },
          [mode](const PickerLocalFileResult& data) {
            return mode == PickerModeType::kUnfocused
                       ? PickerActionType::kOpen
                       : PickerActionType::kInsert;
          },
          [mode](const PickerDriveFileResult& data) {
            return mode == PickerModeType::kUnfocused
                       ? PickerActionType::kOpen
                       : PickerActionType::kInsert;
          },
          [](const PickerCategoryResult& data) {
            return PickerActionType::kDo;
          },
          [](const PickerSearchRequestResult& data) {
            return PickerActionType::kDo;
          },
          [](const PickerEditorResult& data) {
            return PickerActionType::kCreate;
          },
          [](const PickerNewWindowResult& data) {
            return PickerActionType::kDo;
          },
          [](const PickerCapsLockResult& data) {
            return PickerActionType::kDo;
          },
          [&](const PickerCaseTransformResult& data) {
            return PickerActionType::kDo;
          }},
      result);
}

std::vector<PickerEmojiResult> PickerController::GetSuggestedEmoji() {
  CHECK(session_);
  return session_->emoji_suggester.GetSuggestedEmoji();
}

bool PickerController::IsGifsEnabled() {
  CHECK(session_);
  return session_->model.IsGifsEnabled();
}

PrefService* PickerController::GetPrefs() {
  CHECK(client_);
  return client_->GetPrefs();
}

PickerModeType PickerController::GetMode() {
  CHECK(session_);
  return session_->model.GetMode();
}

void PickerController::OnViewIsDeleting(views::View* view) {
  view_observation_.Reset();

  session_.reset();
}

void PickerController::FetchFileThumbnail(const base::FilePath& path,
                                          const gfx::Size& size,
                                          FetchFileThumbnailCallback callback) {
  client_->FetchFileThumbnail(path, size, std::move(callback));
}

PickerController::Session::Session(
    PrefService* prefs,
    ui::TextInputClient* focused_client,
    input_method::ImeKeyboard* ime_keyboard,
    PickerModel::EditorStatus editor_status,
    PickerEmojiSuggester::GetNameCallback get_name)
    : model(prefs, focused_client, ime_keyboard, editor_status),
      emoji_history_model(prefs),
      emoji_suggester(&emoji_history_model, std::move(get_name)),
      session_metrics(prefs) {
  session_metrics.OnStartSession(focused_client);
  feature_usage_metrics.StartUsage();
}

PickerController::Session::~Session() {
  feature_usage_metrics.StopUsage();
}

void PickerController::ShowWidget(base::TimeTicks trigger_event_timestamp,
                                  WidgetTriggerSource trigger_source) {
  show_editor_callback_ = client_->CacheEditorContext();

  ui::TextInputClient* focused_client = GetFocusedTextInputClient();
  input_method::ImeKeyboard& keyboard = GetImeKeyboard();

  if (focused_client &&
      focused_client->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) {
    bool should_enable = !keyboard.IsCapsLockEnabled();
    keyboard.SetCapsLockEnabled(should_enable);
    return;
  }

  session_ = std::make_unique<Session>(
      GetPrefs(), focused_client, &keyboard,
      show_editor_callback_.is_null() ? PickerModel::EditorStatus::kDisabled
                                      : PickerModel::EditorStatus::kEnabled,
      base::BindRepeating(
          [](base::WeakPtr<PickerController> weak_controller,
             std::string_view emoji) -> std::string {
            if (weak_controller == nullptr) {
              return "";
            }
            return weak_controller->search_controller_->GetEmojiName(emoji);
          },
          weak_ptr_factory_.GetWeakPtr()));

  const gfx::Rect anchor_bounds = GetPickerAnchorBounds(
      GetCaretBounds(), GetCursorPoint(), GetFocusedWindowBounds());
  if (trigger_source == WidgetTriggerSource::kFeatureTour &&
      session_->model.GetMode() == PickerModeType::kUnfocused) {
    widget_ = PickerWidget::CreateCentered(this, anchor_bounds,
                                           trigger_event_timestamp);
  } else {
    widget_ =
        PickerWidget::Create(this, anchor_bounds, trigger_event_timestamp);
  }
  widget_->Show();

  view_observation_.Observe(widget_->GetContentsView());
}

void PickerController::CloseWidget() {
  if (!widget_) {
    return;
  }

  session_->session_metrics.SetOutcome(
      PickerSessionMetrics::SessionOutcome::kAbandoned);
  widget_->Close();
}

void PickerController::ShowWidgetPostFeatureTour() {
  ShowWidget(base::TimeTicks::Now(), WidgetTriggerSource::kFeatureTour);
}

std::optional<PickerWebPasteTarget> PickerController::GetWebPasteTarget() {
  return client_ ? client_->GetWebPasteTarget() : std::nullopt;
}

void PickerController::InsertResultOnNextFocus(
    const PickerSearchResult& result) {
  if (!widget_) {
    return;
  }

  // Update emoji history in prefs the result is an emoji/symbol/emoticon.
  CHECK(session_);
  if (auto* data = std::get_if<PickerEmojiResult>(&result);
      data != nullptr && session_->model.should_do_learning()) {
    session_->emoji_history_model.UpdateRecentEmoji(
        EmojiResultTypeToCategory(data->type), base::UTF16ToUTF8(data->text));
  }

  std::visit(
      base::Overloaded{
          [&](PickerRichMedia media) {
            ui::InputMethod* input_method = widget_->GetInputMethod();
            if (input_method == nullptr) {
              return;
            }

            // This cancels the previous request if there was one.
            insert_media_request_ = std::make_unique<PickerInsertMediaRequest>(
                input_method, media, kInsertMediaTimeout,
                base::BindOnce(
                    [](base::WeakPtr<PickerController> weak_controller) {
                      return weak_controller
                                 ? weak_controller->GetWebPasteTarget()
                                 : std::nullopt;
                    },
                    weak_ptr_factory_.GetWeakPtr()),
                base::BindOnce(&PickerController::OnInsertCompleted,
                               weak_ptr_factory_.GetWeakPtr(), media));
          },
          [&](PickerClipboardResult data) {
            // This cancels the previous request if there was one.
            paste_request_ = std::make_unique<PickerPasteRequest>(
                ClipboardHistoryController::Get(),
                aura::client::GetFocusClient(widget_->GetNativeView()),
                data.item_id);
          },
          [](std::monostate) { NOTREACHED(); },
      },
      GetInsertionContentForResult(result));

  session_->session_metrics.SetOutcome(
      PickerSessionMetrics::SessionOutcome::kInsertedOrCopied);
}

void PickerController::OnInsertCompleted(
    const PickerRichMedia& media,
    PickerInsertMediaRequest::Result result) {
  // Fallback to copying to the clipboard on failure.
  if (result != PickerInsertMediaRequest::Result::kSuccess) {
    CopyMediaToClipboard(media);
  }
}

PickerCapsLockPosition PickerController::GetCapsLockPosition() {
  PrefService* prefs = GetPrefs();
  if (prefs == nullptr) {
    return PickerCapsLockPosition::kTop;
  }

  int caps_lock_displayed_count =
      prefs->GetInteger(prefs::kPickerCapsLockDislayedCountPrefName);
  int caps_lock_selected_count =
      prefs->GetInteger(prefs::kPickerCapsLockSelectedCountPrefName);
  float caps_lock_selected_ratio =
      static_cast<float>(caps_lock_selected_count) / caps_lock_displayed_count;

  if (caps_lock_displayed_count < kCapsLockMinimumTopDisplayCount ||
      caps_lock_selected_ratio >= kCapsLockRatioThresholdForTop) {
    return PickerCapsLockPosition::kTop;
  }
  if (caps_lock_selected_ratio >= kCapsLockRatioThresholdForBottom) {
    return PickerCapsLockPosition::kMiddle;
  }
  return PickerCapsLockPosition::kBottom;
}

}  // namespace ash