chromium/ash/picker/search/picker_search_controller.cc

// 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/picker/search/picker_search_controller.h"

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

#include "ash/constants/ash_pref_names.h"
#include "ash/picker/model/picker_search_results_section.h"
#include "ash/picker/search/picker_search_aggregator.h"
#include "ash/picker/search/picker_search_request.h"
#include "ash/picker/views/picker_view_delegate.h"
#include "ash/public/cpp/picker/picker_category.h"
#include "ash/public/cpp/picker/picker_client.h"
#include "base/check_deref.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/common/locale_util.h"
#include "components/prefs/pref_service.h"
#include "ui/base/ime/ash/extension_ime_util.h"

namespace ash {
namespace {

constexpr int kMaxEmojiResults = 3;
constexpr int kMaxSymbolResults = 2;
constexpr int kMaxEmoticonResults = 2;
// These are taken from manifest files in:
// https://source.chromium.org/chromium/chromium/src/+/2be4329930cac782779c5083389b83e09a8bcb47:chrome/browser/resources/chromeos/input_method/
constexpr auto kImeToLangCode =
    base::MakeFixedFlatMap<std::string_view, std::string_view>(
        {{"xkb:us::eng", "en"},           {"xkb:us:intl:eng", "en"},
         {"xkb:us:altgr-intl:eng", "en"}, {"xkb:us:dvorak:eng", "en"},
         {"xkb:us:dvp:eng", "en"},        {"xkb:us:colemak:eng", "en"},
         {"xkb:us:workman:eng", "en"},    {"xkb:us:workman-intl:eng", "en"},
         {"xkb:fr::fra", "fr"},           {"xkb:be::fra", "fr"},
         {"xkb:ca::fra", "fr"},           {"xkb:ch:fr:fra", "fr"},
         {"xkb:ca:multix:fra", "fr"},     {"xkb:de::ger", "de"},
         {"xkb:de:neo:ger", "de"},        {"xkb:be::ger", "de"},
         {"xkb:ch::ger", "de"},           {"xkb:jp::jpn", "ja"},
         {"xkb:ca:eng:eng", "en"},        {"xkb:es::spa", "es"},
         {"xkb:dk::dan", "da"},           {"xkb:latam::spa", "es"},
         {"xkb:gb:extd:eng", "en"},       {"xkb:gb:dvorak:eng", "en"},
         {"xkb:fi::fin", "fi"},           {"xkb:no::nob", "no"},
         {"xkb:se::swe", "sv"},           {"nacl_mozc_us", "ja"},
         {"nacl_mozc_jp", "ja"}});

base::span<const emoji::EmojiSearchEntry> FirstNOrLessElements(
    base::span<const emoji::EmojiSearchEntry> container,
    size_t n) {
  return container.subspan(0, std::min(container.size(), n));
}

const base::Value::Dict* LoadEmojiVariantsFromPrefs(PrefService* prefs) {
  if (prefs == nullptr) {
    return nullptr;
  }
  return prefs->GetDict(prefs::kEmojiPickerPreferences)
      .FindDict("preferred_variants");
}

std::vector<std::string> GetLanguageCodesFromPrefs(PrefService* prefs) {
  if (prefs == nullptr) {
    return {"en"};
  }

  // These codes should be in this order.
  // - First: Current active IME
  // - Remaining: All other IMEs that are enabled
  // There should be no duplicates.
  std::vector<std::string> results;

  // Get the currently active IME
  std::string current_ime = extension_ime_util::GetComponentIDByInputMethodID(
      prefs->GetString(prefs::kLanguageCurrentInputMethod));
  if (const auto& it = kImeToLangCode.find(current_ime);
      it != kImeToLangCode.end()) {
    results.push_back(std::string(it->second));
  }

  // Add ui language as second set of results.
  // EmojiSearch expects the "base language" without region e.g. "en" instead of
  // "en-US".
  std::string ui_locale(language::ExtractBaseLanguage(
      prefs->GetString(language::prefs::kApplicationLocale)));

  results.push_back(ui_locale);

  // All enabled engines
  std::vector<std::string> full_ids =
      base::SplitString(prefs->GetString(prefs::kLanguagePreloadEngines), ",",
                        base::WhitespaceHandling::TRIM_WHITESPACE,
                        base::SplitResult::SPLIT_WANT_ALL);

  for (const std::string& id : full_ids) {
    std::string short_ime_id =
        extension_ime_util::GetComponentIDByInputMethodID(id);
    if (const auto& it = kImeToLangCode.find(short_ime_id);
        it != kImeToLangCode.end() && !base::Contains(results, it->second)) {
      results.push_back(std::string(it->second));
    }
  }

  if (results.empty()) {
    // If no languages supported then attempt to use English.
    results.push_back("en");
  }

  return results;
}

}  // namespace

PickerSearchController::PickerSearchController(PickerClient* client,
                                               base::TimeDelta burn_in_period)
    : client_(CHECK_DEREF(client)), burn_in_period_(burn_in_period) {}

PickerSearchController::~PickerSearchController() = default;

void PickerSearchController::LoadEmojiLanguagesFromPrefs() {
  PrefService* prefs = client_->GetPrefs();
  pref_change_registrar_.Reset();
  if (prefs == nullptr) {
    return;
  }
  emoji_search_.LoadEmojiLanguages(GetLanguageCodesFromPrefs(prefs));

  pref_change_registrar_.Init(prefs);
  pref_change_registrar_.Add(
      prefs::kLanguagePreloadEngines,
      base::BindRepeating(&PickerSearchController::LoadEmojiLanguages,
                          weak_ptr_factory_.GetWeakPtr(),
                          pref_change_registrar_.prefs()));
}

void PickerSearchController::LoadEmojiLanguages(PrefService* prefs) {
  if (prefs == nullptr) {
    return;
  }
  emoji_search_.LoadEmojiLanguages(GetLanguageCodesFromPrefs(prefs));
}

void PickerSearchController::StartSearch(
    std::u16string_view query,
    std::optional<PickerCategory> category,
    PickerSearchRequest::Options search_options,
    PickerViewDelegate::SearchResultsCallback callback) {
  StopSearch();
  aggregator_ = std::make_unique<PickerSearchAggregator>(burn_in_period_,
                                                         std::move(callback));

  // TODO: b/348067874 - Hook `done_closure` up to `aggregator_`.
  search_request_ = std::make_unique<PickerSearchRequest>(
      query, std::move(category),
      base::BindRepeating(&PickerSearchAggregator::HandleSearchSourceResults,
                          aggregator_->GetWeakPtr()),
      base::BindOnce(&PickerSearchAggregator::HandleNoMoreResults,
                     aggregator_->GetWeakPtr()),
      &client_.get(), std::move(search_options));
}

void PickerSearchController::StopSearch() {
  // The search request must be reset first so it can let the aggregator know
  // that it has been interrupted.
  search_request_.reset();
  aggregator_.reset();
}

void PickerSearchController::StartEmojiSearch(
    std::u16string_view query,
    PickerViewDelegate::EmojiSearchResultsCallback callback) {
  const base::TimeTicks search_start = base::TimeTicks::Now();

  emoji::EmojiSearchResult results = emoji_search_.SearchEmoji(
      base::UTF16ToUTF8(query), GetLanguageCodesFromPrefs(client_->GetPrefs()));

  base::TimeDelta elapsed = base::TimeTicks::Now() - search_start;
  base::UmaHistogramTimes("Ash.Picker.Search.EmojiProvider.QueryTime", elapsed);

  std::vector<PickerEmojiResult> emoji_results;
  emoji_results.reserve(kMaxEmojiResults + kMaxSymbolResults +
                        kMaxEmoticonResults);

  const base::Value::Dict* emoji_variants =
      LoadEmojiVariantsFromPrefs(client_->GetPrefs());

  for (const emoji::EmojiSearchEntry& result :
       FirstNOrLessElements(results.emojis, kMaxEmojiResults)) {
    std::string emoji_string = result.emoji_string;
    if (emoji_variants != nullptr) {
      const std::string* variant_string =
          emoji_variants->FindString(emoji_string);
      if (variant_string != nullptr) {
        emoji_string = *variant_string;
      }
    }
    emoji_results.push_back(PickerEmojiResult::Emoji(
        base::UTF8ToUTF16(emoji_string),
        base::UTF8ToUTF16(emoji_search_.GetEmojiName(emoji_string, "en"))));
  }
  for (const emoji::EmojiSearchEntry& result :
       FirstNOrLessElements(results.symbols, kMaxSymbolResults)) {
    emoji_results.push_back(
        PickerEmojiResult::Symbol(base::UTF8ToUTF16(result.emoji_string),
                                  base::UTF8ToUTF16(emoji_search_.GetEmojiName(
                                      result.emoji_string, "en"))));
  }
  for (const emoji::EmojiSearchEntry& result :
       FirstNOrLessElements(results.emoticons, kMaxEmoticonResults)) {
    emoji_results.push_back(PickerEmojiResult::Emoticon(
        base::UTF8ToUTF16(result.emoji_string),
        base::UTF8ToUTF16(
            emoji_search_.GetEmojiName(result.emoji_string, "en"))));
  }

  std::move(callback).Run(std::move(emoji_results));
}

std::string PickerSearchController::GetEmojiName(std::string_view emoji) {
  return emoji_search_.GetEmojiName(emoji, "en");
}

}  // namespace ash