blob: faf3c0ec542f233a8ec06b0aa01ff6ee75dd17bb [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/app_list/search/search_controller.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ui/app_list/app_list_model_updater.h"
#include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "chrome/browser/ui/app_list/search/search_provider.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/app_list_launch_recorder.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/app_search_result_ranker.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/recurrence_ranker.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/search_result_ranker.h"
#include "chrome/browser/ui/ash/tablet_mode_client.h"
#include "third_party/metrics_proto/chrome_os_app_list_launch_event.pb.h"
using metrics::ChromeOSAppListLaunchEventProto;
namespace app_list {
namespace {
constexpr char kLogDisplayTypeClickedResultZeroState[] =
"Apps.LogDisplayTypeClickedResultZeroState";
// TODO(931149): Move the string manipulation utilities into a helper class.
// Normalizes training targets by removing any scheme prefix and trailing slash:
// "arc://[id]/" to "[id]". This is necessary because apps launched from
// different parts of the launcher have differently formatted IDs.
std::string NormalizeId(const std::string& id) {
std::string result(id);
// No existing scheme names include the delimiter string "://".
std::size_t delimiter_index = result.find("://");
if (delimiter_index != std::string::npos)
result.erase(0, delimiter_index + 3);
if (!result.empty() && result.back() == '/')
result.pop_back();
return result;
}
// Remove the Arc app shortcut label from an app ID, if it exists, so that
// "[app]/[label]" becomes "[app]".
std::string RemoveAppShortcutLabel(const std::string& id) {
std::string result(id);
std::size_t delimiter_index = result.find_last_of('/');
if (delimiter_index != std::string::npos)
result.erase(delimiter_index);
return result;
}
} // namespace
SearchController::SearchController(AppListModelUpdater* model_updater,
AppListControllerDelegate* list_controller,
Profile* profile)
: mixer_(std::make_unique<Mixer>(model_updater)),
app_ranker_(std::make_unique<AppSearchResultRanker>(
profile->GetPath(),
chromeos::ProfileHelper::IsEphemeralUserProfile(profile))),
list_controller_(list_controller) {
mixer_->SetNonAppSearchResultRanker(
std::make_unique<SearchResultRanker>(profile));
}
SearchController::~SearchController() {}
void SearchController::Start(const base::string16& query) {
dispatching_query_ = true;
for (const auto& provider : providers_)
provider->Start(query);
dispatching_query_ = false;
last_query_ = query;
query_for_recommendation_ = query.empty();
OnResultsChanged();
}
void SearchController::ViewClosing() {
for (const auto& provider : providers_)
provider->ViewClosing();
}
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;
// Log the display type of the clicked result in zero-state
if (query_for_recommendation_) {
UMA_HISTOGRAM_ENUMERATION(kLogDisplayTypeClickedResultZeroState,
result->display_type(),
ash::SearchResultDisplayType::kLast);
}
result->Open(event_flags);
// Launching apps can take some time. It looks nicer to dismiss the app list.
// Do not close app list for home launcher.
if (!TabletModeClient::Get() ||
!TabletModeClient::Get()->tablet_mode_enabled()) {
list_controller_->DismissView();
}
}
void SearchController::InvokeResultAction(ChromeSearchResult* result,
int action_index,
int event_flags) {
// TODO(xiyuan): Hook up with user learning.
result->InvokeAction(action_index, event_flags);
}
size_t SearchController::AddGroup(size_t max_results,
double multiplier,
double boost) {
return mixer_->AddGroup(max_results, multiplier, boost);
}
void SearchController::AddProvider(size_t group_id,
std::unique_ptr<SearchProvider> provider) {
provider->set_result_changed_callback(
base::Bind(&SearchController::OnResultsChanged, base::Unretained(this)));
mixer_->AddProviderToGroup(group_id, provider.get());
providers_.emplace_back(std::move(provider));
}
void SearchController::OnResultsChanged() {
if (dispatching_query_)
return;
size_t num_max_results =
query_for_recommendation_
? AppListConfig::instance().num_start_page_tiles()
: AppListConfig::instance().max_search_results();
mixer_->MixAndPublish(num_max_results, last_query_);
}
ChromeSearchResult* SearchController::FindSearchResult(
const std::string& result_id) {
for (const auto& provider : providers_) {
for (const auto& result : provider->results()) {
if (result->id() == result_id)
return result.get();
}
}
return nullptr;
}
ChromeSearchResult* SearchController::GetResultByTitleForTest(
const std::string& title) {
base::string16 target_title = base::ASCIIToUTF16(title);
for (const auto& provider : providers_) {
for (const auto& result : provider->results()) {
if (result->title() == target_title &&
result->result_type() == ash::SearchResultType::kInstalledApp &&
result->display_type() !=
ash::SearchResultDisplayType::kRecommendation) {
return result.get();
}
}
}
return nullptr;
}
AppSearchResultRanker* SearchController::GetAppSearchResultRanker() {
return app_ranker_.get();
}
SearchResultRanker* SearchController::GetNonAppSearchResultRanker() {
return mixer_->GetNonAppSearchResultRanker();
}
int SearchController::GetLastQueryLength() const {
return last_query_.size();
}
void SearchController::Train(const std::string& id, RankingItemType type) {
if (app_list_features::IsAppListLaunchRecordingEnabled()) {
ChromeOSAppListLaunchEventProto::LaunchType launch_type;
if (type == RankingItemType::kApp ||
type == RankingItemType::kArcAppShortcut) {
launch_type = ChromeOSAppListLaunchEventProto::APP_TILES;
} else {
launch_type = ChromeOSAppListLaunchEventProto::RESULTS_LIST;
}
// TODO(951287): Record the last-used domain.
AppListLaunchRecorder::GetInstance()->Record(
{launch_type, NormalizeId(id), base::UTF16ToUTF8(last_query_),
std::string(), last_launched_app_id_});
// Only record the last launched app if the hashed logging feature flag is
// enabled, because it is only used by hashed logging.
if (type == RankingItemType::kApp) {
last_launched_app_id_ = NormalizeId(id);
} else if (type == RankingItemType::kArcAppShortcut) {
last_launched_app_id_ = RemoveAppShortcutLabel(NormalizeId(id));
}
}
for (const auto& provider : providers_)
provider->Train(id, type);
mixer_->Train(base::UTF16ToUTF8(last_query_), id, type);
}
} // namespace app_list