chromium/chrome/browser/chromeos/launcher_search/search_util.cc

// Copyright 2022 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/chromeos/launcher_search/search_util.h"

#include <string_view>

#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/omnibox/browser/autocomplete_classifier.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/favicon_cache.h"
#include "components/omnibox/browser/omnibox_feature_configs.h"
#include "components/omnibox/browser/suggestion_answer.h"
#include "components/search_engines/search_terms_data.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/omnibox_proto/answer_type.pb.h"
#include "third_party/omnibox_proto/rich_answer_template.pb.h"
#include "ui/base/page_transition_types.h"

namespace crosapi {
namespace {

using mojom::SearchResult;
using mojom::SearchResultPtr;
using RemoteConsumer = mojo::Remote<crosapi::mojom::SearchResultConsumer>;
using RequestSource = SearchTermsData::RequestSource;

SearchResult::AnswerType MatchTypeToAnswerType(const int type) {
  switch (static_cast<omnibox::AnswerType>(type)) {
    case omnibox::ANSWER_TYPE_WEATHER:
      return SearchResult::AnswerType::kWeather;
    case omnibox::ANSWER_TYPE_CURRENCY:
      return SearchResult::AnswerType::kCurrency;
    case omnibox::ANSWER_TYPE_DICTIONARY:
      return SearchResult::AnswerType::kDictionary;
    case omnibox::ANSWER_TYPE_FINANCE:
      return SearchResult::AnswerType::kFinance;
    case omnibox::ANSWER_TYPE_SUNRISE_SUNSET:
      return SearchResult::AnswerType::kSunrise;
    case omnibox::ANSWER_TYPE_TRANSLATION:
      return SearchResult::AnswerType::kTranslation;
    case omnibox::ANSWER_TYPE_WHEN_IS:
      return SearchResult::AnswerType::kWhenIs;
    default:
      return SearchResult::AnswerType::kDefaultAnswer;
  }
}

SearchResult::OmniboxType MatchTypeToOmniboxType(
    const AutocompleteMatchType::Type type) {
  switch (type) {
    case AutocompleteMatchType::URL_WHAT_YOU_TYPED:
    case AutocompleteMatchType::HISTORY_URL:
    case AutocompleteMatchType::HISTORY_TITLE:
    case AutocompleteMatchType::HISTORY_BODY:
    case AutocompleteMatchType::HISTORY_KEYWORD:
    case AutocompleteMatchType::HISTORY_EMBEDDINGS:
    case AutocompleteMatchType::NAVSUGGEST:
    case AutocompleteMatchType::BOOKMARK_TITLE:
    case AutocompleteMatchType::NAVSUGGEST_PERSONALIZED:
    case AutocompleteMatchType::CLIPBOARD_URL:
    case AutocompleteMatchType::PHYSICAL_WEB_DEPRECATED:
    case AutocompleteMatchType::PHYSICAL_WEB_OVERFLOW_DEPRECATED:
    case AutocompleteMatchType::TAB_SEARCH_DEPRECATED:
    case AutocompleteMatchType::DOCUMENT_SUGGESTION:
    case AutocompleteMatchType::PEDAL:
    case AutocompleteMatchType::HISTORY_CLUSTER:
    case AutocompleteMatchType::STARTER_PACK:
      return SearchResult::OmniboxType::kDomain;

    case AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED:
    case AutocompleteMatchType::SEARCH_SUGGEST:
    case AutocompleteMatchType::SEARCH_SUGGEST_ENTITY:
    case AutocompleteMatchType::SEARCH_SUGGEST_TAIL:
    case AutocompleteMatchType::SEARCH_SUGGEST_PROFILE:
    case AutocompleteMatchType::SEARCH_OTHER_ENGINE:
    case AutocompleteMatchType::CONTACT_DEPRECATED:
    case AutocompleteMatchType::VOICE_SUGGEST:
    case AutocompleteMatchType::CLIPBOARD_TEXT:
    case AutocompleteMatchType::CLIPBOARD_IMAGE:
      return SearchResult::OmniboxType::kSearch;

    case AutocompleteMatchType::SEARCH_HISTORY:
    case AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED:
      return SearchResult::OmniboxType::kHistory;

    case AutocompleteMatchType::OPEN_TAB:
      return SearchResult::OmniboxType::kOpenTab;

    // Currently unhandled enum values.
    // If you came here from a compile error, please contact
    // [email protected] to determine what the correct
    // `OmniboxType` should be.
    case AutocompleteMatchType::EXTENSION_APP_DEPRECATED:
    case AutocompleteMatchType::CALCULATOR:
    case AutocompleteMatchType::NULL_RESULT_MESSAGE:
    case AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH:
    // TILE types seem to be mobile-only.
    case AutocompleteMatchType::TILE_SUGGESTION:
    case AutocompleteMatchType::TILE_NAVSUGGEST:
    case AutocompleteMatchType::TILE_MOST_VISITED_SITE:
    case AutocompleteMatchType::TILE_REPEATABLE_QUERY:
      LOG(ERROR) << "Unhandled AutocompleteMatchType value: "
                 << AutocompleteMatchType::ToString(type);
      return SearchResult::OmniboxType::kDomain;

    case AutocompleteMatchType::NUM_TYPES:
      // NUM_TYPES is not a valid enumerator value, so fall through below.
      break;
  }
  // https://abseil.io/tips/147: Handle non-enumerator values.
  NOTREACHED_IN_MIGRATION()
      << "Unexpected AutocompleteMatchType value: " << static_cast<int>(type);
  return SearchResult::OmniboxType::kDomain;
}

SearchResult::MetricsType MatchTypeToMetricsType(
    AutocompleteMatchType::Type type) {
  switch (type) {
    case AutocompleteMatchType::URL_WHAT_YOU_TYPED:
      return SearchResult::MetricsType::kWhatYouTyped;
    case AutocompleteMatchType::HISTORY_URL:
      // A recently-visited URL that is also a bookmark is handled manually when
      // constructing the result.
      return SearchResult::MetricsType::kRecentlyVisitedWebsite;
    case AutocompleteMatchType::HISTORY_TITLE:
      return SearchResult::MetricsType::kHistoryTitle;
    case AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED:
      return SearchResult::MetricsType::kSearchWhatYouTyped;
    case AutocompleteMatchType::SEARCH_HISTORY:
      return SearchResult::MetricsType::kSearchHistory;
    case AutocompleteMatchType::SEARCH_SUGGEST:
      return SearchResult::MetricsType::kSearchSuggest;
    case AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED:
      return SearchResult::MetricsType::kSearchSuggestPersonalized;
    case AutocompleteMatchType::BOOKMARK_TITLE:
      return SearchResult::MetricsType::kBookmark;
    case AutocompleteMatchType::SEARCH_SUGGEST_ENTITY:
      return SearchResult::MetricsType::kSearchSuggestEntity;
    case AutocompleteMatchType::NAVSUGGEST:
      return SearchResult::MetricsType::kNavSuggest;
    case AutocompleteMatchType::CALCULATOR:
      return SearchResult::MetricsType::kCalculator;
    default:
      return SearchResult::MetricsType::kUnset;
  }
}

SearchResult::TextType TextStyleToType(
    const SuggestionAnswer::TextStyle style) {
  switch (style) {
    case SuggestionAnswer::TextStyle::POSITIVE:
      return SearchResult::TextType::kPositive;
    case SuggestionAnswer::TextStyle::NEGATIVE:
      return SearchResult::TextType::kNegative;
    default:
      return SearchResult::TextType::kUnset;
  }
}

SearchResult::TextType ColorTypeToType(
    omnibox::FormattedString::ColorType type) {
  switch (type) {
    case omnibox::FormattedString::COLOR_ON_SURFACE_POSITIVE:
      return SearchResult::TextType::kPositive;
    case omnibox::FormattedString::COLOR_ON_SURFACE_NEGATIVE:
      return SearchResult::TextType::kNegative;
    default:
      return SearchResult::TextType::kUnset;
  }
}

SearchResult::TextType ClassesToType(
    const ACMatchClassifications& text_classes) {
  // Only retain the URL class, other classes are either ignored. Tag indices
  // are also ignored since they will apply to the entire text.
  for (const auto& text_class : text_classes) {
    if (text_class.style & ACMatchClassification::URL) {
      return SearchResult::TextType::kUrl;
    }
  }

  return SearchResult::TextType::kUnset;
}

SearchResultPtr CreateBaseResult(const AutocompleteMatch& match,
                                 AutocompleteController* controller,
                                 const AutocompleteInput& input) {
  AutocompleteMatch match_copy = match;
  SearchResultPtr result = SearchResult::New();

  if (controller && match_copy.search_terms_args) {
    match_copy.search_terms_args->request_source = RequestSource::CROS_APP_LIST;
    controller->SetMatchDestinationURL(&match_copy);
  }

  result->type = mojom::SearchResultType::kOmniboxResult;
  result->relevance = match_copy.relevance;
  result->destination_url = match_copy.destination_url;

  if (controller && match_copy.stripped_destination_url.spec().empty()) {
    match_copy.ComputeStrippedDestinationURL(
        input,
        controller->autocomplete_provider_client()->GetTemplateURLService());
  }
  result->stripped_destination_url = match_copy.stripped_destination_url;

  if (ui::PageTransitionCoreTypeIs(
          match_copy.transition,
          ui::PageTransition::PAGE_TRANSITION_GENERATED)) {
    result->page_transition = SearchResult::PageTransition::kGenerated;
  } else {
    result->page_transition = SearchResult::PageTransition::kTyped;
  }

  result->is_omnibox_search = AutocompleteMatch::IsSearchType(match_copy.type)
                                  ? SearchResult::OptionalBool::kTrue
                                  : SearchResult::OptionalBool::kFalse;
  return result;
}

}  // namespace

int ProviderTypes() {
  // We use all the default providers except for the document provider,
  // which suggests Drive files on enterprise devices. This is disabled to
  // avoid duplication with search results from DriveFS.
  int providers = AutocompleteClassifier::DefaultOmniboxProviders() &
                  ~AutocompleteProvider::TYPE_DOCUMENT;

  // The open tab provider is not included in the default providers, so add
  // it in manually.
  providers |= AutocompleteProvider::TYPE_OPEN_TAB;

  return providers;
}

int ProviderTypesPicker(bool bookmarks, bool history, bool open_tabs) {
  int providers = 0;

  if (bookmarks) {
    providers |= AutocompleteProvider::TYPE_BOOKMARK;
  }

  if (history) {
    providers |= AutocompleteProvider::TYPE_HISTORY_QUICK |
                 AutocompleteProvider::TYPE_HISTORY_URL |
                 AutocompleteProvider::TYPE_HISTORY_FUZZY |
                 AutocompleteProvider::TYPE_HISTORY_EMBEDDINGS;
  }

  if (open_tabs) {
    providers |= AutocompleteProvider::TYPE_OPEN_TAB;
  }

  return providers;
}

// Convert from our Mojo page transition type into the UI equivalent.
ui::PageTransition PageTransitionToUiPageTransition(
    SearchResult::PageTransition transition) {
  switch (transition) {
    case SearchResult::PageTransition::kTyped:
      return ui::PAGE_TRANSITION_TYPED;
    case SearchResult::PageTransition::kGenerated:
      return ui::PAGE_TRANSITION_GENERATED;
    default:
      NOTREACHED_IN_MIGRATION();
      return ui::PAGE_TRANSITION_FIRST;
  }
}

SearchResultPtr CreateAnswerResult(const AutocompleteMatch& match,
                                   AutocompleteController* controller,
                                   std::u16string_view query,
                                   const AutocompleteInput& input) {
  SearchResultPtr result = CreateBaseResult(match, controller, input);

  result->is_answer = SearchResult::OptionalBool::kTrue;

  // Special case: calculator results (are the only answer results to) have no
  // explicit answer data.
  if (match.answer_type == omnibox::ANSWER_TYPE_UNSPECIFIED) {
    DCHECK_EQ(match.type, AutocompleteMatchType::CALCULATOR);
    result->answer_type = SearchResult::AnswerType::kCalculator;

    // Calculator results come in two forms:
    // 1) Answer in |contents|, empty |description|,
    // 2) Query in |contents|, answer in |description|.
    // For case 1, we should manually populate the query.
    if (match.description.empty()) {
      result->contents = std::u16string(query);
      result->contents_type = mojom::SearchResult::TextType::kUnset;
      result->description = match.contents;
      result->description_type = ClassesToType(match.contents_class);
    } else {
      result->contents = match.contents;
      result->contents_type = ClassesToType(match.contents_class);
      result->description = match.description;
      result->description_type = ClassesToType(match.description_class);
    }

    return result;
  }

  result->answer_type = MatchTypeToAnswerType(match.answer_type);
  result->contents = match.contents;

  if (omnibox_feature_configs::SuggestionAnswerMigration::Get().enabled &&
      match.answer_template) {
    const auto& headline = match.answer_template->answers(0).headline();
    if (headline.fragments_size() > 1) {
      // Only use the second fragment as the first is equivalent to
      // |match.contents|.
      result->additional_contents =
          base::UTF8ToUTF16(headline.fragments(1).text());
      result->additional_contents_type =
          ColorTypeToType(headline.fragments(1).color());
    }
    const auto& subhead = match.answer_template->answers(0).subhead();
    if (subhead.fragments_size() > 0) {
      result->description = base::UTF8ToUTF16(subhead.fragments(0).text());
      result->description_type = ColorTypeToType(subhead.fragments(0).color());
    }
    if (subhead.fragments_size() > 1) {
      result->additional_description =
          base::UTF8ToUTF16(subhead.fragments(1).text());
      result->additional_description_type =
          ColorTypeToType(subhead.fragments(1).color());
    }
    if (result->answer_type == SearchResult::AnswerType::kWeather) {
      result->image_url = GURL(match.answer_template->answers(0).image().url());
      result->description_a11y_label = base::UTF8ToUTF16(subhead.a11y_text());
    }

    return result;
  }

  if (result->answer_type == SearchResult::AnswerType::kWeather) {
    result->image_url = match.answer->image_url();

    const std::u16string* a11y_label =
        match.answer->second_line().accessibility_label();
    if (a11y_label)
      result->description_a11y_label = *a11y_label;
  }

  const auto& first = match.answer->first_line();
  if (first.additional_text()) {
    result->additional_contents = first.additional_text()->text();
    result->additional_contents_type =
        TextStyleToType(first.additional_text()->style());
  }

  const auto& second = match.answer->second_line();
  if (!second.text_fields().empty()) {
    // Only extract the first text field.
    result->description = second.text_fields()[0].text();
    result->description_type = TextStyleToType(second.text_fields()[0].style());
  }
  if (second.additional_text()) {
    result->additional_description = second.additional_text()->text();
    result->additional_description_type =
        TextStyleToType(second.additional_text()->style());
  }

  return result;
}

SearchResultPtr CreateResult(const AutocompleteMatch& match,
                             AutocompleteController* controller,
                             FaviconCache* favicon_cache,
                             bookmarks::BookmarkModel* bookmark_model,
                             const AutocompleteInput& input) {
  SearchResultPtr result = CreateBaseResult(match, controller, input);

  result->metrics_type = MatchTypeToMetricsType(match.type);
  result->is_answer = SearchResult::OptionalBool::kFalse;
  result->contents = match.contents;
  result->contents_type = ClassesToType(match.contents_class);
  result->description = match.description;
  result->description_type = ClassesToType(match.description_class);

  // This may not be the final type. Bookmarks take precedence.
  result->omnibox_type = MatchTypeToOmniboxType(match.type);

  if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_ENTITY &&
      !match.image_url.is_empty()) {
    result->image_url = match.image_url;
  } else {
    // Set the favicon if this result is eligible.
    bool use_favicon =
        result->omnibox_type == SearchResult::OmniboxType::kDomain ||
        result->omnibox_type == SearchResult::OmniboxType::kOpenTab;
    if (use_favicon && favicon_cache) {
      // Provide hook by which a result object can receive an
      // asychronously-fetched favicon. Use a pointer so that our callback can
      // own the remote interface.
      RemoteConsumer consumer;
      result->receiver = consumer.BindNewPipeAndPassReceiver();
      auto emit_favicon = base::BindOnce(
          [](RemoteConsumer consumer, const gfx::Image& icon) {
            consumer->OnFaviconReceived(icon.AsImageSkia());
          },
          std::move(consumer));

      const auto icon = favicon_cache->GetFaviconForPageUrl(
          match.destination_url, std::move(emit_favicon));
      if (!icon.IsEmpty())
        result->favicon = icon.AsImageSkia();
    }

    // Otherwise, set the bookmark type if this result is eligible.
    if (result->favicon.isNull() && bookmark_model &&
        bookmark_model->IsBookmarked(match.destination_url)) {
      result->omnibox_type = SearchResult::OmniboxType::kBookmark;
      result->metrics_type = SearchResult::MetricsType::kBookmark;
    }
  }

  return result;
}

}  // namespace crosapi