blob: b44ef7bf3e4e7050f2c90dacda574e940795e2ad [file] [log] [blame]
// Copyright 2024 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/ui/ash/picker/picker_client_impl.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "ash/picker/picker_controller.h"
#include "ash/public/cpp/picker/picker_search_result.h"
#include "base/check.h"
#include "base/check_deref.h"
#include "base/containers/span.h"
#include "base/files/file_enumerator.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/notimplemented.h"
#include "base/ranges/algorithm.h"
#include "base/ranges/functional.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/files/drive_search_provider.h"
#include "chrome/browser/ash/app_list/search/files/file_search_provider.h"
#include "chrome/browser/ash/app_list/search/omnibox/omnibox_lacros_provider.h"
#include "chrome/browser/ash/app_list/search/omnibox/omnibox_provider.h"
#include "chrome/browser/ash/app_list/search/search_engine.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/input_method/editor_mediator_factory.h"
#include "chrome/browser/chromeos/launcher_search/search_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/picker/picker_file_suggester.h"
#include "chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h"
#include "chrome/browser/ui/webui/ash/emoji/emoji_picker.mojom-forward.h"
#include "chrome/browser/ui/webui/ash/emoji/emoji_picker.mojom-shared.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/storage_partition.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
namespace ash {
enum class AppListSearchResultType;
}
namespace {
constexpr int kMaxGifsToSearch = 4;
bool IsSupportedLocalFileFormat(const base::FilePath& file_path) {
for (std::string_view extension :
{".jpg", ".jpeg", ".png", ".gif", ".webp"}) {
if (file_path.MatchesFinalExtension(extension)) {
return true;
}
}
return false;
}
std::vector<ash::PickerSearchResult> CreateSearchResultsForRecentLocalFiles(
std::vector<PickerFileSuggester::LocalFile> files) {
std::vector<ash::PickerSearchResult> results;
results.reserve(files.size());
for (PickerFileSuggester::LocalFile& file : files) {
if (IsSupportedLocalFileFormat(file.path)) {
results.push_back(ash::PickerSearchResult::LocalFile(
std::move(file.title), std::move(file.path)));
}
}
return results;
}
std::vector<ash::PickerSearchResult> CreateSearchResultsForRecentDriveFiles(
std::vector<PickerFileSuggester::DriveFile> files) {
std::vector<ash::PickerSearchResult> results;
results.reserve(files.size());
for (PickerFileSuggester::DriveFile& file : files) {
results.push_back(ash::PickerSearchResult::DriveFile(std::move(file.title),
std::move(file.url)));
}
return results;
}
std::unique_ptr<app_list::SearchProvider> CreateDriveSearchProvider(
Profile* profile) {
return std::make_unique<app_list::DriveSearchProvider>(profile);
}
std::unique_ptr<app_list::SearchProvider> CreateFileSearchProvider(
Profile* profile) {
return std::make_unique<app_list::FileSearchProvider>(
profile, base::FileEnumerator::FileType::FILES);
}
std::vector<ash::PickerSearchResult> ConvertSearchResults(
std::vector<std::unique_ptr<ChromeSearchResult>> results) {
std::vector<ash::PickerSearchResult> picker_results;
picker_results.reserve(results.size());
for (const std::unique_ptr<ChromeSearchResult>& result : results) {
CHECK(result);
}
base::ranges::sort(results, base::ranges::greater(),
[](const std::unique_ptr<ChromeSearchResult>& result) {
return result->relevance();
});
for (const std::unique_ptr<ChromeSearchResult>& result : results) {
switch (result->result_type()) {
case ash::AppListSearchResultType::kOmnibox:
case ash::AppListSearchResultType::kOpenTab: {
if (std::optional<GURL> result_url = result->url();
result_url.has_value()) {
picker_results.push_back(ash::PickerSearchResult::BrowsingHistory(
*result_url, result->title(), result->icon().icon));
} else {
picker_results.push_back(
ash::PickerSearchResult::Text(result->title()));
}
break;
}
case ash::AppListSearchResultType::kFileSearch: {
// TODO: b/322926411 - Move this filtering to the search provider.
if (IsSupportedLocalFileFormat(result->filePath())) {
picker_results.push_back(ash::PickerSearchResult::LocalFile(
result->title(), result->filePath()));
}
break;
}
case ash::AppListSearchResultType::kDriveSearch:
picker_results.push_back(ash::PickerSearchResult::DriveFile(
result->title(), *result->url()));
break;
default:
LOG(DFATAL) << "Got unexpected search result type "
<< static_cast<int>(result->result_type());
break;
}
}
return picker_results;
}
} // namespace
PickerClientImpl::PickerClientImpl(ash::PickerController* controller,
user_manager::UserManager* user_manager)
: controller_(controller) {
controller_->SetClient(this);
// As `PickerClientImpl` is initialised in
// `ChromeBrowserMainExtraPartsAsh::PostProfileInit`, the user manager does
// not notify us of the first user "change".
ActiveUserChanged(user_manager->GetActiveUser());
user_session_state_observation_.Observe(user_manager);
}
PickerClientImpl::~PickerClientImpl() {
// Calling `PickerController::SetClient` with null requires the old client
// (this client) to be valid. This is fine as we have not started destructing
// anything yet.
controller_->SetClient(nullptr);
}
scoped_refptr<network::SharedURLLoaderFactory>
PickerClientImpl::GetSharedURLLoaderFactory() {
CHECK(profile_);
return profile_->GetURLLoaderFactory();
}
void PickerClientImpl::FetchGifSearch(const std::string& query,
FetchGifsCallback callback) {
CHECK(profile_);
content::StoragePartition* storage_partition =
profile_->GetDefaultStoragePartition();
CHECK(storage_partition);
// This will cancel the previous in-flight request if there is one.
current_gif_fetcher_ = gif_tenor_api_fetcher_.FetchGifSearchCancellable(
base::BindOnce(&PickerClientImpl::OnGifSearchResponse,
weak_factory_.GetWeakPtr(), std::move(callback), query),
storage_partition->GetURLLoaderFactoryForBrowserProcess(), query,
std::nullopt, kMaxGifsToSearch);
current_gif_search_query_ = query;
}
void PickerClientImpl::OnGifSearchResponse(
PickerClientImpl::FetchGifsCallback callback,
std::string gif_search_query,
emoji_picker::mojom::Status status,
emoji_picker::mojom::TenorGifResponsePtr response) {
if (gif_search_query != current_gif_search_query_) {
// Do not call the callback at all if this is an old request.
return;
}
if (status != emoji_picker::mojom::Status::kHttpOk) {
// TODO: b/325368650 - Add better handling of errors.
std::move(callback).Run({});
return;
}
std::vector<ash::PickerSearchResult> picker_results;
CHECK(response);
picker_results.reserve(response->results.size());
for (const emoji_picker::mojom::GifResponsePtr& result : response->results) {
CHECK(result);
const emoji_picker::mojom::GifUrlsPtr& urls = result->url;
CHECK(urls);
picker_results.push_back(ash::PickerSearchResult::Gif(
urls->preview, urls->preview_image, result->preview_size, urls->full,
result->full_size, base::UTF8ToUTF16(result->content_description)));
}
std::move(callback).Run(std::move(picker_results));
}
void PickerClientImpl::StopGifSearch() {
current_gif_fetcher_.reset();
current_gif_search_query_.reset();
}
void PickerClientImpl::StartCrosSearch(
const std::u16string& query,
std::optional<ash::PickerCategory> category,
CrosSearchResultsCallback callback) {
if (!category.has_value()) {
CHECK(search_engine_);
search_engine_->StartSearch(
query, app_list::SearchOptions(),
base::BindRepeating(&PickerClientImpl::OnCrosSearchResultsUpdated,
weak_factory_.GetWeakPtr(), std::move(callback)));
return;
}
switch (*category) {
case ash::PickerCategory::kEditor:
case ash::PickerCategory::kExpressions:
case ash::PickerCategory::kClipboard:
case ash::PickerCategory::kDatesTimes:
case ash::PickerCategory::kUnitsMaths:
case ash::PickerCategory::kUpperCase:
case ash::PickerCategory::kLowerCase:
case ash::PickerCategory::kSentenceCase:
case ash::PickerCategory::kTitleCase:
case ash::PickerCategory::kCapsOn:
case ash::PickerCategory::kCapsOff:
DLOG(FATAL) << "Unexpected category for StartCrosSearch: "
<< static_cast<int>(*category);
break;
case ash::PickerCategory::kLinks:
case ash::PickerCategory::kDriveFiles:
case ash::PickerCategory::kLocalFiles: {
if (filtered_search_engine_ == nullptr ||
current_filter_category_ != category) {
filtered_search_engine_ =
std::make_unique<app_list::SearchEngine>(profile_);
filtered_search_engine_->AddProvider(
CreateSearchProviderForCategory(*category));
current_filter_category_ = category;
}
filtered_search_engine_->StartSearch(
query, app_list::SearchOptions(),
base::BindRepeating(&PickerClientImpl::OnCrosSearchResultsUpdated,
weak_factory_.GetWeakPtr(), std::move(callback)));
} break;
}
}
void PickerClientImpl::OnCrosSearchResultsUpdated(
PickerClientImpl::CrosSearchResultsCallback callback,
ash::AppListSearchResultType result_type,
std::vector<std::unique_ptr<ChromeSearchResult>> results) {
callback.Run(result_type, ConvertSearchResults(std::move(results)));
}
void PickerClientImpl::OnZeroStateLinksSearchResultsUpdated(
PickerClientImpl::SuggestedLinksCallback callback,
ash::AppListSearchResultType result_type,
std::vector<std::unique_ptr<ChromeSearchResult>> results) {
callback.Run(ConvertSearchResults(std::move(results)));
}
void PickerClientImpl::StopCrosQuery() {
CHECK(search_engine_);
search_engine_->StopQuery();
}
void PickerClientImpl::ShowEditor() {
auto* editor_mediator =
ash::input_method::EditorMediatorFactory::GetInstance()->GetForProfile(
profile_);
if (editor_mediator == nullptr) {
return;
}
editor_mediator->HandleTrigger();
}
void PickerClientImpl::GetRecentLocalFileResults(RecentFilesCallback callback) {
file_suggester_->GetRecentLocalFiles(
base::BindOnce(CreateSearchResultsForRecentLocalFiles)
.Then(std::move(callback)));
}
void PickerClientImpl::GetRecentDriveFileResults(RecentFilesCallback callback) {
file_suggester_->GetRecentDriveFiles(
base::BindOnce(CreateSearchResultsForRecentDriveFiles)
.Then(std::move(callback)));
}
void PickerClientImpl::GetSuggestedLinkResults(
SuggestedLinksCallback callback) {
// TODO: b/330938446 - Replace with proper zero-state logic.
if (zero_state_links_search_engine_ == nullptr) {
zero_state_links_search_engine_ =
std::make_unique<app_list::SearchEngine>(profile_);
zero_state_links_search_engine_->AddProvider(CreateOmniboxProvider(
/*bookmarks=*/true, /*history=*/true, /*open_tabs=*/true));
}
zero_state_links_search_engine_->StartSearch(
u"http", app_list::SearchOptions(),
base::BindRepeating(
&PickerClientImpl::OnZeroStateLinksSearchResultsUpdated,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void PickerClientImpl::ActiveUserChanged(user_manager::User* active_user) {
if (!active_user) {
SetProfile(nullptr);
return;
}
active_user->AddProfileCreatedObserver(
base::BindOnce(&PickerClientImpl::SetProfileByUser,
weak_factory_.GetWeakPtr(), active_user));
}
void PickerClientImpl::SetProfileByUser(const user_manager::User* user) {
Profile* profile = Profile::FromBrowserContext(
ash::BrowserContextHelper::Get()->GetBrowserContextByUser(user));
SetProfile(profile);
}
void PickerClientImpl::SetProfile(Profile* profile) {
if (profile_ == profile) {
return;
}
profile_ = profile;
search_engine_ = std::make_unique<app_list::SearchEngine>(profile_);
search_engine_->AddProvider(CreateOmniboxProvider(
/*bookmarks=*/true, /*history=*/true, /*open_tabs=*/true));
search_engine_->AddProvider(CreateFileSearchProvider(profile_));
search_engine_->AddProvider(CreateDriveSearchProvider(profile_));
zero_state_links_search_engine_.reset();
file_suggester_ = std::make_unique<PickerFileSuggester>(profile_);
}
std::unique_ptr<app_list::SearchProvider>
PickerClientImpl::CreateOmniboxProvider(bool bookmarks,
bool history,
bool open_tabs) {
if (crosapi::browser_util::IsLacrosEnabled()) {
return std::make_unique<app_list::OmniboxLacrosProvider>(
profile_, &app_list_controller_delegate_,
PickerLacrosOmniboxSearchProvider::CreateControllerCallback(
bookmarks, history, open_tabs));
} else {
return std::make_unique<app_list::OmniboxProvider>(
profile_, &app_list_controller_delegate_,
crosapi::ProviderTypesPicker(bookmarks, history, open_tabs));
}
}
std::unique_ptr<app_list::SearchProvider>
PickerClientImpl::CreateSearchProviderForCategory(
ash::PickerCategory category) {
switch (category) {
case ash::PickerCategory::kEditor:
case ash::PickerCategory::kExpressions:
case ash::PickerCategory::kClipboard:
case ash::PickerCategory::kDatesTimes:
case ash::PickerCategory::kUnitsMaths:
case ash::PickerCategory::kUpperCase:
case ash::PickerCategory::kLowerCase:
case ash::PickerCategory::kSentenceCase:
case ash::PickerCategory::kTitleCase:
case ash::PickerCategory::kCapsOn:
case ash::PickerCategory::kCapsOff:
DLOG(FATAL) << "Unexpected category for autocomplete: "
<< static_cast<int>(category);
return nullptr;
case ash::PickerCategory::kLinks:
return CreateOmniboxProvider(/*bookmarks=*/true, /*history=*/true,
/*open_tabs=*/true);
case ash::PickerCategory::kDriveFiles:
return CreateDriveSearchProvider(profile_);
case ash::PickerCategory::kLocalFiles:
return CreateFileSearchProvider(profile_);
}
}
PickerClientImpl::PickerAppListControllerDelegate::
PickerAppListControllerDelegate() = default;
PickerClientImpl::PickerAppListControllerDelegate::
~PickerAppListControllerDelegate() = default;
void PickerClientImpl::PickerAppListControllerDelegate::DismissView() {
NOTIMPLEMENTED_LOG_ONCE();
}
aura::Window*
PickerClientImpl::PickerAppListControllerDelegate::GetAppListWindow() {
NOTIMPLEMENTED_LOG_ONCE();
return nullptr;
}
int64_t
PickerClientImpl::PickerAppListControllerDelegate::GetAppListDisplayId() {
NOTIMPLEMENTED_LOG_ONCE();
return 0;
}
bool PickerClientImpl::PickerAppListControllerDelegate::IsAppPinned(
const std::string& app_id) {
NOTIMPLEMENTED_LOG_ONCE();
return false;
}
bool PickerClientImpl::PickerAppListControllerDelegate::IsAppOpen(
const std::string& app_id) const {
NOTIMPLEMENTED_LOG_ONCE();
return false;
}
void PickerClientImpl::PickerAppListControllerDelegate::PinApp(
const std::string& app_id) {
NOTIMPLEMENTED_LOG_ONCE();
}
void PickerClientImpl::PickerAppListControllerDelegate::UnpinApp(
const std::string& app_id) {
NOTIMPLEMENTED_LOG_ONCE();
}
AppListControllerDelegate::Pinnable
PickerClientImpl::PickerAppListControllerDelegate::GetPinnable(
const std::string& app_id) {
NOTIMPLEMENTED_LOG_ONCE();
return AppListControllerDelegate::NO_PIN;
}
void PickerClientImpl::PickerAppListControllerDelegate::CreateNewWindow(
bool incognito,
bool should_trigger_session_restore) {
NOTIMPLEMENTED_LOG_ONCE();
}
void PickerClientImpl::PickerAppListControllerDelegate::OpenURL(
Profile* profile,
const GURL& url,
ui::PageTransition transition,
WindowOpenDisposition disposition) {
NOTIMPLEMENTED_LOG_ONCE();
}