chromium/chrome/browser/ash/app_list/search/ranking/answer_ranker.cc

// Copyright 2021 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/ash/app_list/search/ranking/answer_ranker.h"

#include "ash/public/cpp/app_list/app_list_types.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/common/icon_constants.h"
#include "chrome/browser/ash/app_list/search/types.h"

namespace app_list {
namespace {

// Returns the provider type's priority. A higher value indicates higher
// priority. Providers that are never used for answers will have 0 priority.
int GetPriority(ProviderType type) {
  switch (type) {
    case ProviderType::kOmnibox:
      return 3;
    case ProviderType::kKeyboardShortcut:
      return 2;
      // TODO(b/263994165): Check if this is the correct priority.
    case ProviderType::kSystemInfo:
      return 1;
    default:
      return 0;
  }
}

// If there are any Omnibox answers, returns the highest scoring one. If not,
// returns nullptr.
ChromeSearchResult* GetOmniboxCandidate(Results& results) {
  ChromeSearchResult* top_answer = nullptr;
  double top_score = 0.0;
  for (const auto& result : results) {
    if (result->display_type() != DisplayType::kAnswerCard) {
      continue;
    }

    const double score = result->relevance();
    if (!top_answer || score > top_score) {
      top_answer = result.get();
      top_score = score;
    }
  }
  return top_answer;
}

// If there are any best match SystemInfo answers, returns the highest scoring
// one. If not, returns nullptr.
ChromeSearchResult* GetSystemInfoCandidate(Results& results) {
  ChromeSearchResult* top_answer = nullptr;
  double top_score = 0.0;
  for (const auto& result : results) {
    if (result->display_type() != DisplayType::kAnswerCard ||
        !result->best_match()) {
      continue;
    }

    const double score = result->relevance();
    if (!top_answer || score > top_score) {
      top_answer = result.get();
      top_score = score;
    }
  }
  return top_answer;
}

// Returns the Shortcut best match as long as there is only one. Otherwise,
// returns nullptr.
ChromeSearchResult* GetShortcutCandidate(Results& results) {
  ChromeSearchResult* best_shortcut = nullptr;
  for (auto& result : results) {
    if (!result->best_match()) {
      continue;
    }

    if (best_shortcut) {
      // A best match shortcut has already been found, so there are at least
      // two and neither should be promoted to Answer Card.
      return nullptr;
    }
    best_shortcut = result.get();
  }
  return best_shortcut;
}

}  // namespace

AnswerRanker::AnswerRanker() = default;

AnswerRanker::~AnswerRanker() = default;

void AnswerRanker::Start(const std::u16string& query,
                         CategoriesList& categories) {
  burn_in_elapsed_ = false;
  chosen_answer_ = nullptr;
  omnibox_candidates_.clear();
}

void AnswerRanker::UpdateResultRanks(ResultsMap& results,
                                     ProviderType provider) {
  if (GetPriority(provider) == 0) {
    return;
  }

  const auto it = results.find(provider);
  DCHECK(it != results.end());
  auto& new_results = it->second;

  // Keep track of Omnibox candidates. Any candidates that are not selected
  // should be filtered out later.
  if (provider == ProviderType::kOmnibox) {
    for (const auto& result : new_results) {
      if (result->display_type() == DisplayType::kAnswerCard) {
        omnibox_candidates_.push_back(result.get());
      }
    }
  }

  // Don't change a selected answer after the burn-in period has elapsed. This
  // includes ensuring that the answer is re-selected.
  if (burn_in_elapsed_ && chosen_answer_) {
    PromoteChosenAnswer();
    return;
  }

  // Don't make any changes if the chosen answer has higher priority than the
  // current provider.
  if (chosen_answer_ &&
      GetPriority(provider) < GetPriority(chosen_answer_->result_type())) {
    return;
  }

  // Finally, choose a new candidate from the current provider if one exists.
  ChromeSearchResult* new_answer = nullptr;
  switch (provider) {
    case ProviderType::kOmnibox:
      new_answer = GetOmniboxCandidate(new_results);
      break;
    case ProviderType::kKeyboardShortcut:
      new_answer = GetShortcutCandidate(new_results);
      break;
    case ProviderType::kSystemInfo:
      new_answer = GetSystemInfoCandidate(new_results);
      break;
    default:
      return;
  }
  if (new_answer) {
    chosen_answer_ = new_answer->GetWeakPtr();
  }

  if (burn_in_elapsed_) {
    PromoteChosenAnswer();
  }
}

void AnswerRanker::OnBurnInPeriodElapsed() {
  burn_in_elapsed_ = true;
  PromoteChosenAnswer();
}

void AnswerRanker::PromoteChosenAnswer() {
  if (!chosen_answer_) {
    return;
  }

  // Filter out unsuccessful Omnibox candidates.
  for (ChromeSearchResult* result : omnibox_candidates_) {
    if (result && result->id() != chosen_answer_->id()) {
      result->scoring().set_filtered(true);
    }
  }

  chosen_answer_->SetDisplayType(DisplayType::kAnswerCard);
  chosen_answer_->SetMultilineTitle(true);
  if (chosen_answer_->result_type() == ResultType::kSystemInfo) {
    chosen_answer_->SetIconDimension(kSystemAnswerCardIconDimension);
  } else {
    chosen_answer_->SetIconDimension(kAnswerCardIconDimension);
  }
}

}  // namespace app_list