// 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/constants/ash_features.h"
#include "ash/picker/picker_controller.h"
#include "ash/public/cpp/picker/picker_search_result.h"
#include "ash/public/cpp/picker/picker_web_paste_target.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/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/ranking/ranker_manager.h"
#include "chrome/browser/ash/app_list/search/search_engine.h"
#include "chrome/browser/ash/app_list/search/types.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/ash/picker/picker_link_suggester.h"
#include "chrome/browser/ui/ash/picker/picker_thumbnail_loader.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "chromeos/components/editor_menu/public/cpp/preset_text_query.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_iterator.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "ui/aura/window.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/gfx/native_widget_types.h"
#include "url/gurl.h"
namespace ash {
enum class AppListSearchResultType;
}
namespace {
// TODO: b/345303965 - Finalize this string.
constexpr std::u16string_view kAnnouncementViewName = u"Picker";
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> CreateSearchResultsForRecentLocalImages(
std::vector<PickerFileSuggester::LocalFile> files) {
std::vector<ash::PickerSearchResult> results;
results.reserve(files.size());
for (PickerFileSuggester::LocalFile& file : files) {
results.push_back(ash::PickerLocalFileResult(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::PickerDriveFileResult(std::move(file.id), std::move(file.title),
std::move(file.url), file.local_path));
}
return results;
}
std::unique_ptr<app_list::SearchProvider> CreateDriveSearchProvider(
Profile* profile) {
auto provider = std::make_unique<app_list::DriveSearchProvider>(
profile, /*should_filter_shared_files=*/false,
/*should_filter_directories=*/true);
if (base::FeatureList::IsEnabled(ash::features::kPickerCloud)) {
provider->SetQuerySource(
drivefs::mojom::QueryParameters::QuerySource::kCloudOnly);
}
return provider;
}
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 (result->metrics_type() == ash::OMNIBOX_URL_WHAT_YOU_TYPED) {
continue;
}
if (std::optional<GURL> result_url = result->url();
result_url.has_value()) {
picker_results.push_back(ash::PickerBrowsingHistoryResult(
*result_url, result->title(), result->icon().icon,
result->best_match()));
} else {
picker_results.push_back(ash::PickerTextResult(
result->title(), ash::PickerTextResult::Source::kOmnibox));
}
break;
}
case ash::AppListSearchResultType::kFileSearch: {
// TODO: b/322926411 - Move this filtering to the search provider.
if (IsSupportedLocalFileFormat(result->filePath())) {
picker_results.push_back(ash::PickerLocalFileResult(
result->title(), result->filePath(), result->best_match()));
}
break;
}
case ash::AppListSearchResultType::kDriveSearch:
picker_results.push_back(ash::PickerDriveFileResult(
result->DriveId(), result->title(), *result->url(),
result->filePath(), result->best_match()));
break;
default:
LOG(DFATAL) << "Got unexpected search result type "
<< static_cast<int>(result->result_type());
break;
}
}
return picker_results;
}
ash::input_method::EditorMediator* GetEditorMediator(Profile* profile) {
if (!chromeos::features::IsOrcaEnabled()) {
return nullptr;
}
return ash::input_method::EditorMediatorFactory::GetInstance()->GetForProfile(
profile);
}
// TODO: b/326847990 - Remove this once it's moved to mojom traits.
chromeos::editor_menu::PresetQueryCategory FromMojoPresetQueryCategory(
const crosapi::mojom::EditorPanelPresetQueryCategory category) {
using EditorPanelPresetQueryCategory =
crosapi::mojom::EditorPanelPresetQueryCategory;
using PresetQueryCategory = chromeos::editor_menu::PresetQueryCategory;
switch (category) {
case EditorPanelPresetQueryCategory::kUnknown:
return PresetQueryCategory::kUnknown;
case EditorPanelPresetQueryCategory::kShorten:
return PresetQueryCategory::kShorten;
case EditorPanelPresetQueryCategory::kElaborate:
return PresetQueryCategory::kElaborate;
case EditorPanelPresetQueryCategory::kRephrase:
return PresetQueryCategory::kRephrase;
case EditorPanelPresetQueryCategory::kFormalize:
return PresetQueryCategory::kFormalize;
case EditorPanelPresetQueryCategory::kEmojify:
return PresetQueryCategory::kEmojify;
case EditorPanelPresetQueryCategory::kProofread:
return PresetQueryCategory::kProofread;
}
}
std::vector<ash::PickerSearchResult> GetEditorResultsFromPanelContext(
crosapi::mojom::EditorPanelContextPtr panel_context) {
std::vector<ash::PickerSearchResult> results;
for (const crosapi::mojom::EditorPanelPresetTextQueryPtr& query :
panel_context->preset_text_queries) {
results.push_back(ash::PickerEditorResult(
ash::PickerEditorResult::Mode::kRewrite, base::UTF8ToUTF16(query->name),
FromMojoPresetQueryCategory(query->category), query->text_query_id));
}
return results;
}
app_list::CategoriesList CreateRankerCategories() {
app_list::CategoriesList res({{.category = app_list::Category::kWeb},
{.category = app_list::Category::kFiles}});
return res;
}
} // namespace
PickerClientImpl::PickerClientImpl(ash::PickerController* controller,
user_manager::UserManager* user_manager)
: announcer_(kAnnouncementViewName), 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);
}
void PickerClientImpl::StartCrosSearch(
const std::u16string& query,
std::optional<ash::PickerCategory> category,
CrosSearchResultsCallback callback) {
ranker_categories_ = CreateRankerCategories();
ranker_manager_->Start(query, ranker_categories_);
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::kEditorWrite:
case ash::PickerCategory::kEditorRewrite:
case ash::PickerCategory::kEmojisGifs:
case ash::PickerCategory::kEmojis:
case ash::PickerCategory::kClipboard:
case ash::PickerCategory::kDatesTimes:
case ash::PickerCategory::kUnitsMaths:
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) {
app_list::ResultsMap results_map;
results_map[result_type] = std::move(results);
if (ranker_manager_ != nullptr) {
ranker_manager_->UpdateResultRanks(results_map, result_type);
}
callback.Run(result_type,
ConvertSearchResults(std::move(results_map[result_type])));
}
void PickerClientImpl::StopCrosQuery() {
CHECK(search_engine_);
search_engine_->StopQuery();
}
bool PickerClientImpl::IsEligibleForEditor() {
ash::input_method::EditorMediator* editor_mediator =
GetEditorMediator(profile_);
if (editor_mediator == nullptr) {
return false;
}
return editor_mediator->GetEditorMode() !=
ash::input_method::EditorMode::kHardBlocked;
}
PickerClientImpl::ShowEditorCallback PickerClientImpl::CacheEditorContext() {
ash::input_method::EditorMediator* editor_mediator =
GetEditorMediator(profile_);
if (editor_mediator == nullptr) {
return {};
}
editor_mediator->CacheContext();
ash::input_method::EditorMode editor_mode = editor_mediator->GetEditorMode();
if (editor_mode == ash::input_method::EditorMode::kSoftBlocked ||
editor_mode == ash::input_method::EditorMode::kHardBlocked) {
return {};
}
return base::BindOnce(&PickerClientImpl::ShowEditor,
weak_factory_.GetWeakPtr());
}
void PickerClientImpl::GetSuggestedEditorResults(
SuggestedEditorResultsCallback callback) {
ash::input_method::EditorMediator* editor_mediator =
GetEditorMediator(profile_);
if (editor_mediator == nullptr ||
editor_mediator->panel_manager() == nullptr) {
std::move(callback).Run({});
return;
}
ash::input_method::EditorMode editor_mode = editor_mediator->GetEditorMode();
if (editor_mode == ash::input_method::EditorMode::kHardBlocked ||
editor_mode == ash::input_method::EditorMode::kSoftBlocked) {
std::move(callback).Run({});
return;
}
editor_mediator->panel_manager()->GetEditorPanelContext(
base::BindOnce(GetEditorResultsFromPanelContext)
.Then(std::move(callback)));
}
void PickerClientImpl::GetRecentLocalFileResults(size_t max_files,
RecentFilesCallback callback) {
file_suggester_->GetRecentLocalImages(
max_files, base::BindOnce(CreateSearchResultsForRecentLocalImages)
.Then(std::move(callback)));
}
void PickerClientImpl::GetRecentDriveFileResults(size_t max_files,
RecentFilesCallback callback) {
file_suggester_->GetRecentDriveFiles(
max_files, base::BindOnce(CreateSearchResultsForRecentDriveFiles)
.Then(std::move(callback)));
}
void PickerClientImpl::GetSuggestedLinkResults(
size_t max_results,
SuggestedLinksCallback callback) {
link_suggester_->GetSuggestedLinks(max_results, std::move(callback));
}
bool PickerClientImpl::IsFeatureAllowedForDogfood() {
return gaia::IsGoogleInternalAccountEmail(profile_->GetProfileUserName());
}
void PickerClientImpl::FetchFileThumbnail(const base::FilePath& path,
const gfx::Size& size,
FetchFileThumbnailCallback callback) {
CHECK(thumbnail_loader_);
thumbnail_loader_->Load(path, size, std::move(callback));
}
PrefService* PickerClientImpl::GetPrefs() {
return profile_ == nullptr ? nullptr : profile_->GetPrefs();
}
// Forked from `ClipboardHistoryControllerDelegateImpl::Paste`.
std::optional<ash::PickerWebPasteTarget> PickerClientImpl::GetWebPasteTarget() {
std::unique_ptr<content::RenderWidgetHostIterator> widgets =
content::RenderWidgetHost::GetRenderWidgetHosts();
while (content::RenderWidgetHost* rwh = widgets->GetNextHost()) {
content::RenderViewHost* rvh = content::RenderViewHost::From(rwh);
if (rvh == nullptr) {
continue;
}
content::WebContents* web_contents =
content::WebContents::FromRenderViewHost(rvh);
if (web_contents == nullptr) {
continue;
}
if (web_contents->GetPrimaryMainFrame()->GetRenderViewHost() != rvh) {
continue;
}
content::RenderFrameHost* focused_frame = web_contents->GetFocusedFrame();
if (focused_frame == nullptr) {
continue;
}
content::WebContents* focused_web_contents =
content::WebContents::FromRenderFrameHost(focused_frame);
if (focused_web_contents == nullptr) {
continue;
}
gfx::NativeView window = focused_web_contents->GetContentNativeView();
if (window == nullptr) {
continue;
}
if (!window->HasFocus()) {
continue;
}
return std::make_optional<ash::PickerWebPasteTarget>(
focused_web_contents->GetLastCommittedURL(),
// SAFETY: Callers must call this synchronously as per the
// documentation, so this `base::Unretained` is safe.
base::BindOnce(&content::WebContents::Paste,
base::Unretained(focused_web_contents)));
}
return std::nullopt;
}
void PickerClientImpl::Announce(std::u16string_view message) {
announcer_.Announce(std::u16string(message));
}
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_));
ranker_manager_ = std::make_unique<app_list::RankerManager>(profile_);
file_suggester_ = std::make_unique<PickerFileSuggester>(profile_);
link_suggester_ = std::make_unique<PickerLinkSuggester>(profile_);
thumbnail_loader_ = std::make_unique<PickerThumbnailLoader>(profile_);
if (controller_ != nullptr) {
controller_->OnClientProfileSet();
}
}
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::kEditorWrite:
case ash::PickerCategory::kEditorRewrite:
case ash::PickerCategory::kEmojisGifs:
case ash::PickerCategory::kEmojis:
case ash::PickerCategory::kClipboard:
case ash::PickerCategory::kDatesTimes:
case ash::PickerCategory::kUnitsMaths:
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_);
}
}
void PickerClientImpl::ShowEditor(std::optional<std::string> preset_query_id,
std::optional<std::string> freeform_text) {
ash::input_method::EditorMediator* editor_mediator =
GetEditorMediator(profile_);
if (editor_mediator != nullptr) {
editor_mediator->HandleTrigger(std::move(preset_query_id),
std::move(freeform_text));
}
}
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();
}