Commit 22a1e477 authored by tby's avatar tby Committed by Commit Bot

[Files Ranking] Add zero-state files search provider.

This adds the first of our new search providers for the files
ranking project, which provides zero-state local files suggestions
to the CrOS launcher. The design doc can be found here:

go/cros-zero-state-files-ranker

A quick overview:

 - The ZeroStateFileProvider is a new search provider that tracks
   recently-used files to display in zero state.

 - It does this by feeding notifictions from the FileTasksObserver
   into a RecurrenceRanker (Dolphin) model.

 - It's unrelated to the LauncherSearchProvider, which provides file
   results for search queries. The LSP only provides results given a
   query, and the ZSFP only provides results without one, so these
   are mutually exclusive. Note this has been implemented separately
   from the LSP so as not to further tie the launcher to the Files
   app API and javascript.

This CL has some TODOs related to parallel CLs: the search result
icon needs to be set, DriveFS results need to be filtered out, and
UMA metrics added.

The use of the new provider is gated behind a disabled-by-default
feature flag. We'll enable this once the rest of the files ranking
code has been merged.

Bug: 959679
Change-Id: I7b9697906cf13793e34fc79097b379cbe8becb48
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1755623Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarJia Meng <jiameng@chromium.org>
Commit-Queue: Tony Yeoman <tby@chromium.org>
Cr-Commit-Position: refs/heads/master@{#688643}
parent 762fa5d7
......@@ -78,6 +78,8 @@ enum SearchResultType {
// A result from omnibox for the personalized suggestion.
// Currently, it is used for the user's recent query.
OMNIBOX_SUGGEST_PERSONALIZED,
// A zero-state result representing a local file.
ZERO_STATE_FILE,
// Boundary is always last.
SEARCH_RESULT_TYPE_BOUNDARY
};
......
......@@ -113,16 +113,17 @@ enum class AppListLaunchType {
// Type of the search result, which is set in Chrome.
enum class SearchResultType {
kUnknown, // Unknown type. Don't use over IPC
kInstalledApp, // Installed apps.
kPlayStoreApp, // Installable apps from PlayStore.
kInstantApp, // Instant apps.
kInternalApp, // Chrome OS apps.
kOmnibox, // Results from Omnibox.
kLauncher, // Results from launcher search (currently only from Files).
kAnswerCard, // WebContents based answer card.
kUnknown, // Unknown type. Don't use over IPC
kInstalledApp, // Installed apps.
kPlayStoreApp, // Installable apps from PlayStore.
kInstantApp, // Instant apps.
kInternalApp, // Chrome OS apps.
kOmnibox, // Results from Omnibox.
kLauncher, // Results from launcher search (currently only from Files).
kAnswerCard, // WebContents based answer card.
kPlayStoreReinstallApp, // Reinstall recommendations from PlayStore.
kArcAppShortcut, // ARC++ app shortcuts.
kZeroStateFile, // Zero state local file results.
// Add new values here.
};
......
......@@ -3492,6 +3492,10 @@ jumbo_split_static_library("ui") {
"app_list/search/settings_shortcut/settings_shortcut_provider.h",
"app_list/search/settings_shortcut/settings_shortcut_result.cc",
"app_list/search/settings_shortcut/settings_shortcut_result.h",
"app_list/search/zero_state_file_provider.cc",
"app_list/search/zero_state_file_provider.h",
"app_list/search/zero_state_file_result.cc",
"app_list/search/zero_state_file_result.h",
]
deps += [
# TODO(wutao): Put new icons resources to ash/public/cpp/vector_icons/
......
# Mainly for ML search-ranking purposes, see app_list/OWNERS for other reviews.
per-file search_controller*=jiameng@chromium.org
per-file mixer*=jiameng@chromium.org
per-file zero_state_files_search_provider*=jiameng@chromium.org
per-file zero_state_file_result*=jiameng@chromium.org
# COMPONENT: UI>Shell>Launcher
......@@ -27,6 +27,7 @@
#include "chrome/browser/ui/app_list/search/search_controller.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/search_result_ranker.h"
#include "chrome/browser/ui/app_list/search/settings_shortcut/settings_shortcut_provider.h"
#include "chrome/browser/ui/app_list/search/zero_state_file_provider.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "components/arc/arc_util.h"
......@@ -43,6 +44,7 @@ namespace {
constexpr size_t kMaxAppsGroupResults = 7;
constexpr size_t kMaxOmniboxResults = 4;
constexpr size_t kMaxLauncherSearchResults = 2;
constexpr size_t kMaxZeroStateFileResults = 6;
constexpr size_t kMaxAppReinstallSearchResults = 1;
// We show up to 6 Play Store results. However, part of Play Store results may
// be filtered out because they may correspond to already installed Web apps. So
......@@ -61,6 +63,7 @@ constexpr size_t kMaxSettingsShortcutResults = 6;
constexpr float kBoostOfSettingsShortcut = 10.0f;
constexpr float kBoostOfApps = 8.0f;
constexpr float kBoostOfZeroStateFileResults = 1.0f;
} // namespace
......@@ -153,6 +156,13 @@ std::unique_ptr<SearchController> CreateSearchController(
kMaxAppShortcutResults, profile, list_controller));
}
if (app_list_features::IsZeroStateMixedTypesRankerEnabled()) {
size_t zero_state_files_group_id = controller->AddGroup(
kMaxZeroStateFileResults, 1.0, kBoostOfZeroStateFileResults);
controller->AddProvider(zero_state_files_group_id,
std::make_unique<ZeroStateFileProvider>(profile));
}
return controller;
}
......
......@@ -82,6 +82,8 @@ RankingItemType RankingItemTypeFromSearchResult(
return RankingItemType::kIgnored;
case ash::SearchResultType::kArcAppShortcut:
return RankingItemType::kArcAppShortcut;
case ash::SearchResultType::kZeroStateFile:
return RankingItemType::kZeroStateFile;
}
}
......
......@@ -26,7 +26,8 @@ enum class RankingItemType {
kOmniboxDocument,
kOmniboxHistory,
kOmniboxNavSuggest,
kOmniboxSearch
kOmniboxSearch,
kZeroStateFile
};
// Convert a |ChromeSearchResult| into its |RankingItemType|.
......
// Copyright 2019 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/zero_state_file_provider.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_task_environment.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/search/zero_state_file_result.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace app_list {
namespace {
using ::file_manager::file_tasks::FileTasksObserver;
using ::testing::UnorderedElementsAre;
MATCHER_P(Title, title, "") {
return base::UTF16ToUTF8(arg->title()) == title;
}
} // namespace
class ZeroStateFileProviderTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
profile_ = std::make_unique<TestingProfile>();
provider_ = std::make_unique<ZeroStateFileProvider>(profile_.get());
Wait();
}
base::FilePath Path(const std::string& filename) {
return temp_dir_.GetPath().AppendASCII(filename);
}
void WriteFile(const std::string& filename) {
CHECK_NE(base::WriteFile(Path(filename), "abcd", 4), -1);
CHECK(base::PathExists(Path(filename)));
Wait();
}
FileTasksObserver::FileOpenEvent OpenEvent(const std::string& filename) {
FileTasksObserver::FileOpenEvent e;
e.path = Path(filename);
e.open_type = FileTasksObserver::OpenType::kOpen;
return e;
}
void Wait() { thread_bundle_.RunUntilIdle(); }
content::TestBrowserThreadBundle thread_bundle_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<Profile> profile_;
std::unique_ptr<ZeroStateFileProvider> provider_;
};
TEST_F(ZeroStateFileProviderTest, NoResultsWithQuery) {
provider_->Start(base::UTF8ToUTF16("query"));
Wait();
EXPECT_TRUE(provider_->results().empty());
}
TEST_F(ZeroStateFileProviderTest, Simple) {
WriteFile("exists_1.txt");
WriteFile("exists_2.png");
WriteFile("exists_3.pdf");
provider_->OnFilesOpened(
{OpenEvent("exists_1.txt"), OpenEvent("exists_2.png")});
provider_->OnFilesOpened({OpenEvent("nonexistant.txt")});
provider_->Start(base::string16());
Wait();
EXPECT_THAT(
provider_->results(),
UnorderedElementsAre(Title("exists_1.txt"), Title("exists_2.png")));
}
} // namespace app_list
// Copyright 2019 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/zero_state_file_result.h"
#include "base/files/file_path.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace app_list {
class ZeroStateFileResultTest : public testing::Test {
public:
ZeroStateFileResultTest() {
TestingProfile::Builder profile_builder;
profile_ = profile_builder.Build();
}
~ZeroStateFileResultTest() override = default;
content::TestBrowserThreadBundle thread_bundle_;
std::unique_ptr<Profile> profile_;
};
TEST_F(ZeroStateFileResultTest, CheckMetadata) {
ZeroStateFileResult result(base::FilePath("/my/test/MIXED_case_FILE.Pdf"),
0.2f, profile_.get());
EXPECT_EQ(base::UTF16ToUTF8(result.title()),
std::string("MIXED_case_FILE.Pdf"));
EXPECT_EQ(result.id(), "zerostatefile:///my/test/MIXED_case_FILE.Pdf");
EXPECT_EQ(result.relevance(), 0.2f);
}
} // namespace app_list
// Copyright 2019 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/zero_state_file_provider.h"
#include <string>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/task_runner_util.h"
#include "base/threading/scoped_blocking_call.h"
#include "chrome/browser/chromeos/file_manager/file_tasks_notifier.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/recurrence_ranker.h"
#include "chrome/browser/ui/app_list/search/zero_state_file_result.h"
using file_manager::file_tasks::FileTasksObserver;
namespace app_list {
namespace {
using StringResults = std::vector<std::pair<std::string, float>>;
using FilePathResults = std::vector<std::pair<base::FilePath, float>>;
constexpr int kMaxLocalFiles = 10;
// Given the output of RecurrenceRanker::RankTopN, filter out all results that
// don't exist on disk. Returns the filtered results, with each result string
// converted to a base::FilePath.
FilePathResults FilterNonexistentFiles(const StringResults& ranker_results) {
FilePathResults results;
for (const auto& path_score : ranker_results) {
// We use FilePath::FromUTF8Unsafe to decode the filepath string. As per its
// documentation, this is a safe use of the function because
// ZeroStateFileProvider is only used on ChromeOS, for which
// filepaths are UTF8.
const auto& path = base::FilePath::FromUTF8Unsafe(path_score.first);
if (base::PathExists(path))
results.emplace_back(path, path_score.second);
}
return results;
}
} // namespace
ZeroStateFileProvider::ZeroStateFileProvider(Profile* profile)
: profile_(profile), file_tasks_observer_(this), weak_factory_(this) {
DCHECK(profile_);
task_runner_ = base::CreateSequencedTaskRunner(
{base::ThreadPool(), base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
// TODO(crbug.com/959679): Add a metric for if this succeeds or fails.
if (auto* notifier =
file_manager::file_tasks::FileTasksNotifier::GetForProfile(
profile_)) {
file_tasks_observer_.Add(notifier);
RecurrenceRankerConfigProto config;
config.set_min_seconds_between_saves(300u);
config.set_condition_limit(1u);
config.set_condition_decay(0.5f);
config.set_target_limit(200);
config.set_target_decay(0.9f);
config.mutable_predictor()->mutable_default_predictor();
files_ranker_ = std::make_unique<RecurrenceRanker>(
"ZeroStateLocalFiles",
profile->GetPath().AppendASCII("zero_state_local_files.pb"), config,
chromeos::ProfileHelper::IsEphemeralUserProfile(profile));
}
}
ZeroStateFileProvider::~ZeroStateFileProvider() = default;
void ZeroStateFileProvider::Start(const base::string16& query) {
// TODO(crbug.com/959679): Add latency metrics.
ClearResultsSilently();
if (!query.empty())
return;
base::PostTaskAndReplyWithResult(
task_runner_.get(), FROM_HERE,
base::BindOnce(&FilterNonexistentFiles,
files_ranker_->RankTopN(kMaxLocalFiles)),
base::BindOnce(&ZeroStateFileProvider::SetSearchResults,
weak_factory_.GetWeakPtr()));
}
void ZeroStateFileProvider::SetSearchResults(FilePathResults results) {
SearchProvider::Results new_results;
for (const auto& filepath_score : results) {
new_results.emplace_back(std::make_unique<ZeroStateFileResult>(
filepath_score.first, filepath_score.second, profile_));
}
SwapResults(&new_results);
}
void ZeroStateFileProvider::OnFilesOpened(
const std::vector<FileOpenEvent>& file_opens) {
// TODO(crbug.com/959679): Filter out DriveFS files.
if (!files_ranker_)
return;
for (const auto& file_open : file_opens)
files_ranker_->Record(file_open.path.value());
}
} // namespace app_list
// Copyright 2019 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.
#ifndef CHROME_BROWSER_UI_APP_LIST_SEARCH_ZERO_STATE_FILE_PROVIDER_H_
#define CHROME_BROWSER_UI_APP_LIST_SEARCH_ZERO_STATE_FILE_PROVIDER_H_
#include <map>
#include <memory>
#include <utility>
#include <vector>
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observer.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/string16.h"
#include "chrome/browser/chromeos/file_manager/file_tasks_observer.h"
#include "chrome/browser/ui/app_list/search/search_provider.h"
class Profile;
namespace file_manager {
namespace file_tasks {
class FileTasksNotifier;
}
} // namespace file_manager
namespace app_list {
class RecurrenceRanker;
// ZeroStateFileProvider dispatches queries to extensions and fetches the
// results from them via chrome.launcherSearchProvider API.
class ZeroStateFileProvider : public SearchProvider,
file_manager::file_tasks::FileTasksObserver {
public:
explicit ZeroStateFileProvider(Profile* profile);
~ZeroStateFileProvider() override;
void Start(const base::string16& query) override;
// file_manager::file_tasks::FileTaskObserver:
void OnFilesOpened(const std::vector<FileOpenEvent>& file_opens) override;
private:
// Converts |results| into ZeroStateFilesResults and sets them as this
// provider's results.
void SetSearchResults(std::vector<std::pair<base::FilePath, float>> results);
// The reference to profile to get ZeroStateFileProvider service.
Profile* const profile_;
// The ranking model used to produce local file results for searches with an
// empty query.
std::unique_ptr<RecurrenceRanker> files_ranker_;
ScopedObserver<file_manager::file_tasks::FileTasksNotifier,
file_manager::file_tasks::FileTasksObserver>
file_tasks_observer_;
SEQUENCE_CHECKER(sequence_checker_);
scoped_refptr<base::SequencedTaskRunner> task_runner_;
base::WeakPtrFactory<ZeroStateFileProvider> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(ZeroStateFileProvider);
};
} // namespace app_list
#endif // CHROME_BROWSER_UI_APP_LIST_SEARCH_ZERO_STATE_FILE_PROVIDER_H_
// Copyright 2019 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/zero_state_file_result.h"
#include <utility>
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/i18n/rtl.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/platform_util.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
namespace app_list {
namespace {
constexpr char kZeroStateFilePrefix[] = "zerostatefile://";
}
ZeroStateFileResult::ZeroStateFileResult(const base::FilePath& filepath,
float relevance,
Profile* profile)
: filepath_(filepath), profile_(profile) {
DCHECK(profile);
set_id(kZeroStateFilePrefix + filepath.value());
set_relevance(relevance);
SetTitle(base::UTF8ToUTF16(filepath.BaseName().value()));
SetResultType(ResultType::kZeroStateFile);
SetDisplayType(DisplayType::kList);
// For consistency with LauncherSearchProvider results, set the details to the
// display name of the Files app.
base::string16 sanitized_name = base::CollapseWhitespace(
l10n_util::GetStringUTF16(IDS_FILEMANAGER_APP_NAME), true);
base::i18n::SanitizeUserSuppliedString(&sanitized_name);
SetDetails(sanitized_name);
// TODO(crbug.com/955893): Set the icon.
}
ZeroStateFileResult::~ZeroStateFileResult() = default;
void ZeroStateFileResult::Open(int event_flags) {
platform_util::OpenItem(profile_, filepath_,
platform_util::OpenItemType::OPEN_FILE,
platform_util::OpenOperationCallback());
}
SearchResultType ZeroStateFileResult::GetSearchResultType() const {
return ZERO_STATE_FILE;
}
::std::ostream& operator<<(::std::ostream& os,
const ZeroStateFileResult& result) {
return os << "{" << result.title() << ", " << result.relevance() << "}";
}
} // namespace app_list
// Copyright 2019 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.
#ifndef CHROME_BROWSER_UI_APP_LIST_SEARCH_ZERO_STATE_FILE_RESULT_H_
#define CHROME_BROWSER_UI_APP_LIST_SEARCH_ZERO_STATE_FILE_RESULT_H_
#include <iosfwd>
#include "base/files/file_path.h"
#include "base/macros.h"
#include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "url/gurl.h"
class Profile;
namespace app_list {
// A search result representing a local file for zero state search results.
class ZeroStateFileResult : public ChromeSearchResult {
public:
ZeroStateFileResult(const base::FilePath& filepath,
float relevance,
Profile* profile);
~ZeroStateFileResult() override;
// ChromeSearchResult overrides:
void Open(int event_flags) override;
SearchResultType GetSearchResultType() const override;
private:
void Initialize();
const base::FilePath filepath_;
Profile* const profile_;
DISALLOW_COPY_AND_ASSIGN(ZeroStateFileResult);
};
::std::ostream& operator<<(::std::ostream& os,
const ZeroStateFileResult& result);
} // namespace app_list
#endif // CHROME_BROWSER_UI_APP_LIST_SEARCH_ZERO_STATE_FILE_RESULT_H_
......@@ -5050,6 +5050,8 @@ test("unit_tests") {
"../browser/ui/app_list/search/tests/tokenized_string_char_iterator_unittest.cc",
"../browser/ui/app_list/search/tests/tokenized_string_match_unittest.cc",
"../browser/ui/app_list/search/tests/tokenized_string_unittest.cc",
"../browser/ui/app_list/search/tests/zero_state_file_provider_unittest.cc",
"../browser/ui/app_list/search/tests/zero_state_file_result_unittest.cc",
"../browser/ui/app_list/test/fake_app_list_model_updater.cc",
"../browser/ui/app_list/test/fake_app_list_model_updater.h",
]
......
......@@ -2338,6 +2338,7 @@ Unknown properties are collapsed to zero. -->
<int value="26" label="Omnibox, Search History"/>
<int value="27" label="Omnibox, Search Suggest"/>
<int value="28" label="Omnibox, Personalzied Suggestion"/>
<int value="29" label="Zero State Local File"/>
</enum>
<enum name="AppListSearchResultDisplayType">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment