// 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/search_controller.h"
#include <algorithm>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/app_list/app_list_controller.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_metrics.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/system/federated/federated_service_controller_impl.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/search/app_search_data_source.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/common/file_util.h"
#include "chrome/browser/ash/app_list/search/common/keyword_util.h"
#include "chrome/browser/ash/app_list/search/common/string_util.h"
#include "chrome/browser/ash/app_list/search/common/types_util.h"
#include "chrome/browser/ash/app_list/search/ranking/ranker_manager.h"
#include "chrome/browser/ash/app_list/search/ranking/sorting.h"
#include "chrome/browser/ash/app_list/search/search_engine.h"
#include "chrome/browser/ash/app_list/search/search_features.h"
#include "chrome/browser/ash/app_list/search/search_file_scanner.h"
#include "chrome/browser/ash/app_list/search/search_metrics_manager.h"
#include "chrome/browser/ash/app_list/search/search_provider.h"
#include "chrome/browser/ash/app_list/search/search_session_metrics_manager.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/metrics/structured/event_logging_features.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "ui/display/screen.h"
namespace app_list {
namespace {
void ClearNonZeroStateResults(ResultsMap& results) {
for (auto it = results.begin(); it != results.end();) {
if (!ash::IsZeroStateResultType(it->first)) {
it = results.erase(it);
} else {
++it;
}
}
}
} // namespace
SearchController::SearchController(
AppListModelUpdater* model_updater,
AppListControllerDelegate* list_controller,
ash::AppListNotifier* notifier,
Profile* profile,
ash::federated::FederatedServiceController* federated_service_controller)
: profile_(profile),
model_updater_(model_updater),
list_controller_(list_controller),
notifier_(notifier),
federated_service_controller_(federated_service_controller) {}
SearchController::~SearchController() = default;
void SearchController::Initialize() {
burn_in_controller_ = std::make_unique<BurnInController>(base::BindRepeating(
&SearchController::OnBurnInPeriodElapsed, base::Unretained(this)));
ranker_manager_ = std::make_unique<RankerManager>(profile_);
metrics_manager_ =
std::make_unique<SearchMetricsManager>(profile_, notifier_);
session_metrics_manager_ =
std::make_unique<SearchSessionMetricsManager>(profile_, notifier_);
federated_metrics_manager_ =
std::make_unique<federated::FederatedMetricsManager>(
notifier_, federated_service_controller_);
app_search_data_source_ = std::make_unique<AppSearchDataSource>(
profile_, list_controller_, base::DefaultClock::GetInstance());
app_discovery_metrics_manager_ =
std::make_unique<AppDiscoveryMetricsManager>(profile_);
search_engine_ = std::make_unique<SearchEngine>(profile_);
if (search_features::IsLauncherSearchFileScanEnabled()) {
search_file_scanner_ = std::make_unique<SearchFileScanner>(
profile_, file_manager::util::GetMyFilesFolderForProfile(profile_),
GetTrashPaths(profile_));
}
}
std::vector<ash::AppListSearchControlCategory>
SearchController::GetToggleableCategories() const {
return toggleable_categories_;
}
void SearchController::OnBurnInPeriodElapsed() {
ranker_manager_->OnBurnInPeriodElapsed();
Publish();
}
void SearchController::AddProvider(std::unique_ptr<SearchProvider> provider) {
if (ash::IsZeroStateResultType(provider->ResultType())) {
++total_zero_state_blockers_;
}
// TODO(b/315709613): Temporary. Update the factory.
const auto control_category =
MapSearchCategoryToControlCategory(provider->search_category());
if (control_category != ControlCategory::kCannotToggle &&
std::find(toggleable_categories_.begin(), toggleable_categories_.end(),
control_category) == toggleable_categories_.end()) {
toggleable_categories_.push_back(control_category);
std::sort(toggleable_categories_.begin(), toggleable_categories_.end());
}
search_engine_->AddProvider(std::move(provider));
}
void SearchController::StartSearch(const std::u16string& query) {
DCHECK(!query.empty());
burn_in_controller_->Start();
// TODO(b/266468933): This logging is limited to a short maximum query
// length. Add another metric which measures the bucket count of query length,
// with no maximum.
ash::RecordLauncherIssuedSearchQueryLength(query.length());
// Limit query length, for efficiency reasons in matching query to texts.
const std::u16string truncated_query =
query.length() > kMaxAllowedQueryLength
? query.substr(0, kMaxAllowedQueryLength)
: query;
// Clear all search results but preserve zero-state results.
ClearNonZeroStateResults(results_);
// NOTE: Not publishing change to clear results when the search query changes
// so the old results stay on screen until the new ones are ready.
if (last_query_.empty()) {
Publish();
}
categories_ = CreateAllCategories();
SearchOptions search_options;
if (ash::features::IsLauncherSearchControlEnabled()) {
search_options.search_categories = std::vector<SearchCategory>();
base::flat_set<ControlCategory> disabled_categories;
for (const auto category : toggleable_categories_) {
if (!IsControlCategoryEnabled(profile_, category)) {
disabled_categories.insert(category);
}
}
for (const auto category : search_engine_->GetAllSearchCategories()) {
if (!disabled_categories.contains(
MapSearchCategoryToControlCategory(category))) {
search_options.search_categories->push_back(category);
}
}
}
ranker_manager_->Start(truncated_query, categories_);
session_start_ = base::Time::Now();
last_query_ = truncated_query;
search_engine_->StartSearch(truncated_query, std::move(search_options),
base::BindRepeating(&SearchController::SetResults,
base::Unretained(this)));
}
void SearchController::ClearSearch() {
// Cancel a pending search publish if it exists.
burn_in_controller_->Stop();
ClearNonZeroStateResults(results_);
last_query_.clear();
search_engine_->StopQuery();
Publish();
ranker_manager_->Start(u"", categories_);
}
void SearchController::StartZeroState(base::OnceClosure on_done,
base::TimeDelta timeout) {
// Clear all results - zero state search request is made when the app list
// gets first shown, which would indicate that search is not currently active.
results_.clear();
burn_in_controller_->Stop();
// Categories currently are not used by zero-state, but may be required for
// sorting in SetResults.
categories_ = CreateAllCategories();
ranker_manager_->Start(std::u16string(), categories_);
last_query_.clear();
on_zero_state_done_.AddUnsafe(std::move(on_done));
returned_zero_state_blockers_ = 0;
search_engine_->StartZeroState(base::BindRepeating(
&SearchController::SetResults, base::Unretained(this)));
zero_state_timeout_.Start(
FROM_HERE, timeout,
base::BindOnce(&SearchController::OnZeroStateTimedOut,
base::Unretained(this)));
}
void SearchController::OnZeroStateTimedOut() {
// `on_zero_state_done_` will be empty if all zero-state blocking providers
// have returned. If it isn't, publish whatever results have been returned.
// If `last_query_` is non-empty, this indicates that a search query has been
// issued since zero state results were requested. Do not publish results in
// this case to avoid interfering with queried search burn-in period.
// Zero state callbacks will get run when next batch of results gets
// published.
if (last_query_.empty() && !on_zero_state_done_.empty()) {
Publish();
}
}
void SearchController::AppListViewChanging(bool is_visible) {
// In tablet mode, the launcher is always visible so do not log launcher open
// if the device is in tablet mode.
if (is_visible && !display::Screen::GetScreen()->InTabletMode()) {
app_discovery_metrics_manager_->OnLauncherOpen();
}
// On close.
if (!is_visible) {
search_engine_->StopZeroState();
}
}
void SearchController::OpenResult(ChromeSearchResult* result, int event_flags) {
// This can happen in certain circumstances due to races. See
// https://crbug.com/534772
if (!result) {
return;
}
metrics_manager_->OnOpen(result->result_type(), last_query_);
app_discovery_metrics_manager_->OnOpenResult(result, last_query_);
const bool dismiss_view_on_open = result->dismiss_view_on_open();
// Open() may cause |result| to be deleted.
result->Open(event_flags);
// Launching apps can take some time. It looks nicer to eagerly dismiss the
// app list if |result| permits it. Do not close app list for home launcher.
if (dismiss_view_on_open && !display::Screen::GetScreen()->InTabletMode()) {
list_controller_->DismissView();
}
}
void SearchController::InvokeResultAction(ChromeSearchResult* result,
ash::SearchResultActionType action) {
if (!result) {
return;
}
if (action == ash::SearchResultActionType::kRemove) {
ranker_manager_->Remove(result);
// We need to update the currently published results to not include the
// just-removed result. Manually set the result as filtered and re-publish.
result->scoring().set_filtered(true);
Publish();
}
}
void SearchController::SetResults(ResultType result_type, Results results) {
// Re-post onto the UI sequence if not called from there.
auto ui_thread = content::GetUIThreadTaskRunner({});
if (!ui_thread->RunsTasksInCurrentSequence()) {
ui_thread->PostTask(
FROM_HERE,
base::BindOnce(&SearchController::SetResults, base::Unretained(this),
result_type, std::move(results)));
return;
}
results_[result_type] = std::move(results);
if (ash::IsZeroStateResultType(result_type)) {
SetZeroStateResults(result_type);
} else {
SetSearchResults(result_type);
}
if (results_changed_callback_for_test_) {
results_changed_callback_for_test_.Run(result_type);
}
}
void SearchController::SetSearchResults(ResultType result_type) {
Rank(result_type);
for (const auto& result : results_[result_type]) {
metrics_manager_->OnSearchResultsUpdated(result->scoring());
}
bool is_post_burn_in =
burn_in_controller_->UpdateResults(results_, categories_, result_type);
// If the burn-in period has not yet elapsed, don't call Publish here (this
// case is covered by a call scheduled within the burn-in controller).
if (!last_query_.empty() && is_post_burn_in) {
Publish();
}
}
void SearchController::SetZeroStateResults(ResultType result_type) {
Rank(result_type);
if (ash::IsZeroStateResultType(result_type)) {
++returned_zero_state_blockers_;
}
// Don't publish zero-state results if a queried search is currently in
// progress.
if (!last_query_.empty()) {
return;
}
// Wait until all zero state providers have returned before publishing
// results.
if (!on_zero_state_done_.empty() &&
returned_zero_state_blockers_ < total_zero_state_blockers_) {
return;
}
Publish();
}
void SearchController::Rank(ProviderType provider_type) {
DCHECK(ranker_manager_);
if (results_.empty()) {
// Happens if the burn-in period has elapsed without any results having been
// received from providers. Return early.
return;
}
if (disable_ranking_for_test_) {
return;
}
// Update ranking of all results and categories for this provider. This
// ordering is important, as result scores may affect category scores.
ranker_manager_->UpdateResultRanks(results_, provider_type);
ranker_manager_->UpdateCategoryRanks(results_, categories_, provider_type);
}
void SearchController::Publish() {
SortCategories(categories_);
// Create a vector of category enums in display order.
std::vector<Category> category_enums;
for (const auto& category : categories_) {
category_enums.push_back(category.category);
}
// Compile a single list of results and sort first by their category with best
// match first, then by burn-in iteration number, and finally by relevance.
std::vector<raw_ptr<ChromeSearchResult, VectorExperimental>> all_results;
for (const auto& type_results : results_) {
for (const auto& result : type_results.second) {
double score = result->scoring().FinalScore();
// Filter out results with negative relevance, which is the rankers'
// signal that a result should not be displayed at all.
if (score < 0.0) {
continue;
}
// The display score is the result's final score before display. It is
// used for sorting below, and may be used directly in ash.
result->SetDisplayScore(score);
all_results.push_back(result.get());
}
}
SortResults(all_results, categories_);
if (!observer_list_.empty()) {
std::vector<const ChromeSearchResult*> observer_results;
for (ChromeSearchResult* result : all_results) {
observer_results.push_back(const_cast<const ChromeSearchResult*>(result));
}
std::vector<KeywordInfo> extracted_keyword_info =
ExtractKeywords(last_query_);
for (Observer& observer : observer_list_) {
observer.OnResultsAdded(last_query_, extracted_keyword_info,
observer_results);
}
}
model_updater_->PublishSearchResults(all_results, category_enums);
if (!on_zero_state_done_.empty() &&
(!zero_state_timeout_.IsRunning() ||
returned_zero_state_blockers_ >= total_zero_state_blockers_)) {
on_zero_state_done_.Notify();
}
}
void SearchController::Train(LaunchData&& launch_data) {
// For non-zero state results (i.e. non continue section results), record the
// last search query.
const std::string query = ash::IsZeroStateResultType(launch_data.result_type)
? ""
: base::UTF16ToUTF8(last_query_);
launch_data.query = query;
if (app_list_features::IsAppListLaunchRecordingEnabled()) {
metrics_manager_->OnTrain(launch_data, query);
}
profile_->GetPrefs()->SetBoolean(ash::prefs::kLauncherResultEverLaunched,
true);
// Train all search result ranking models.
ranker_manager_->Train(launch_data);
}
AppSearchDataSource* SearchController::GetAppSearchDataSource() {
return app_search_data_source_.get();
}
ChromeSearchResult* SearchController::FindSearchResult(
const std::string& result_id) {
for (const auto& provider_results : results_) {
for (const auto& result : provider_results.second) {
if (result->id() == result_id) {
return result.get();
}
}
}
return nullptr;
}
void SearchController::AddObserver(Observer* observer) {
observer_list_.AddObserver(observer);
}
void SearchController::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void SearchController::OnDefaultSearchIsGoogleSet(bool is_google) {
federated_metrics_manager_->OnDefaultSearchIsGoogleSet(is_google);
}
std::u16string SearchController::get_query() {
return last_query_;
}
base::Time SearchController::session_start() {
return session_start_;
}
size_t SearchController::ReplaceProvidersForResultTypeForTest(
ash::AppListSearchResultType result_type,
std::unique_ptr<SearchProvider> new_provider) {
return search_engine_->ReplaceProvidersForResultTypeForTest(
result_type, std::move(new_provider));
}
ChromeSearchResult* SearchController::GetResultByTitleForTest(
const std::string& title) {
std::u16string target_title = base::ASCIIToUTF16(title);
for (const auto& provider_results : results_) {
for (const auto& result : provider_results.second) {
if (result->title() == target_title &&
result->result_type() ==
ash::AppListSearchResultType::kInstalledApp &&
!result->is_recommendation()) {
return result.get();
}
}
}
return nullptr;
}
void SearchController::WaitForZeroStateCompletionForTest(
base::OnceClosure callback) {
if (on_zero_state_done_.empty()) {
std::move(callback).Run();
return;
}
on_zero_state_done_.AddUnsafe(std::move(callback));
}
void SearchController::set_results_changed_callback_for_test(
ResultsChangedCallback callback) {
results_changed_callback_for_test_ = std::move(callback);
}
void SearchController::disable_ranking_for_test() {
disable_ranking_for_test_ = true;
}
} // namespace app_list