// Copyright 2020 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/holding_space/holding_space_client_impl.h"
#include <memory>
#include <optional>
#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/holding_space/holding_space_metrics.h"
#include "ash/public/cpp/holding_space/holding_space_progress.h"
#include "base/barrier_closure.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/open_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/clipboard/clipboard_util.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_util.h"
#include "components/drive/drive_pref_names.h"
#include "components/prefs/pref_service.h"
#include "net/base/mime_util.h"
#include "storage/browser/file_system/file_system_context.h"
namespace ash {
namespace {
using ItemLaunchFailureReason = holding_space_metrics::ItemLaunchFailureReason;
// Helpers ---------------------------------------------------------------------
// Returns the `HoldingSpaceKeyedService` associated with the given `profile`.
HoldingSpaceKeyedService* GetHoldingSpaceKeyedService(Profile* profile) {
return HoldingSpaceKeyedServiceFactory::GetInstance()->GetService(profile);
}
// Returns file info for the specified `file_path` or `std::nullopt` in the
// event that file info cannot be obtained.
using GetFileInfoCallback =
base::OnceCallback<void(const std::optional<base::File::Info>&)>;
void GetFileInfo(Profile* profile,
const base::FilePath& file_path,
GetFileInfoCallback callback) {
scoped_refptr<storage::FileSystemContext> file_system_context =
file_manager::util::GetFileManagerFileSystemContext(profile);
file_manager::util::GetMetadataForPath(
file_system_context, file_path,
{storage::FileSystemOperation::GetMetadataField::kIsDirectory,
storage::FileSystemOperation::GetMetadataField::kSize},
base::BindOnce(
[](GetFileInfoCallback callback, base::File::Error error,
const base::File::Info& info) {
std::move(callback).Run(error == base::File::FILE_OK
? std::make_optional<>(info)
: std::nullopt);
},
std::move(callback)));
}
// Opens an in-progress item and returns the reason for failure if any. Returns
// `std::nullopt` if successful. Runs the command `kOpenItem` if there is one;
// otherwise, opens `item` when the underlying download completes.
std::optional<ItemLaunchFailureReason> OpenInProgressItem(
Profile* profile,
const HoldingSpaceItem& item,
holding_space_metrics::EventSource event_source) {
CHECK(!item.progress().IsComplete());
auto command_iter = base::ranges::find(
item.in_progress_commands(), HoldingSpaceCommandId::kOpenItem,
&HoldingSpaceItem::InProgressCommand::command_id);
if (command_iter != item.in_progress_commands().end()) {
command_iter->handler.Run(&item, command_iter->command_id, event_source);
return std::nullopt;
}
return GetHoldingSpaceKeyedService(profile)->OpenItemWhenComplete(&item);
}
// Returns the reason for failing to launch a holding space item for the
// specified open operation `result`. Returns `std::nullopt` on success.
std::optional<ItemLaunchFailureReason> ToItemLaunchFailureReason(
platform_util::OpenOperationResult result) {
switch (result) {
case platform_util::OpenOperationResult::OPEN_SUCCEEDED:
return std::nullopt;
case platform_util::OpenOperationResult::OPEN_FAILED_PATH_NOT_FOUND:
return ItemLaunchFailureReason::kPathNotFound;
case platform_util::OpenOperationResult::OPEN_FAILED_INVALID_TYPE:
return ItemLaunchFailureReason::kInvalidType;
case platform_util::OpenOperationResult::
OPEN_FAILED_NO_HANLDER_FOR_FILE_TYPE:
return ItemLaunchFailureReason::kNoHandlerForFileType;
case platform_util::OpenOperationResult::OPEN_FAILED_FILE_ERROR:
return ItemLaunchFailureReason::kFileError;
}
}
} // namespace
// HoldingSpaceClientImpl ------------------------------------------------------
HoldingSpaceClientImpl::HoldingSpaceClientImpl(Profile* profile)
: profile_(profile) {}
HoldingSpaceClientImpl::~HoldingSpaceClientImpl() = default;
const std::string& HoldingSpaceClientImpl::AddItemOfType(
HoldingSpaceItem::Type type,
const base::FilePath& file_path) {
return GetHoldingSpaceKeyedService(profile_)->AddItemOfType(type, file_path);
}
void HoldingSpaceClientImpl::CopyImageToClipboard(
const HoldingSpaceItem& item,
holding_space_metrics::EventSource event_source,
SuccessCallback callback) {
holding_space_metrics::RecordItemAction(
{&item}, holding_space_metrics::ItemAction::kCopy, event_source);
std::string ext = item.file().file_path.Extension();
std::string mime_type;
if (ext.empty() ||
!net::GetWellKnownMimeTypeFromExtension(ext.substr(1), &mime_type) ||
!net::MatchesMimeType(kMimeTypeImage, mime_type)) {
std::move(callback).Run(/*success=*/false);
return;
}
// Reading and decoding of the image file needs to be done on an I/O thread.
base::ThreadPool::PostTaskAndReply(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&clipboard_util::ReadFileAndCopyToClipboardLocal,
item.file().file_path),
base::BindOnce(
[](SuccessCallback callback) {
// We don't currently receive a signal regarding whether image
// decoding was successful or not. For the time being, assume
// success when the task runs until proven otherwise.
std::move(callback).Run(/*success=*/true);
},
std::move(callback)));
}
base::FilePath HoldingSpaceClientImpl::CrackFileSystemUrl(
const GURL& file_system_url) const {
return file_manager::util::GetFileManagerFileSystemContext(profile_)
->CrackURLInFirstPartyContext(file_system_url)
.path();
}
bool HoldingSpaceClientImpl::IsDriveDisabled() const {
return profile_->GetPrefs()->GetBoolean(drive::prefs::kDisableDrive);
}
void HoldingSpaceClientImpl::OpenDownloads(SuccessCallback callback) {
auto file_path = file_manager::util::GetDownloadsFolderForProfile(profile_);
if (file_path.empty()) {
std::move(callback).Run(/*success=*/false);
return;
}
file_manager::util::OpenItem(
profile_, file_path, platform_util::OPEN_FOLDER,
base::BindOnce(
[](SuccessCallback callback,
platform_util::OpenOperationResult result) {
const bool success = result == platform_util::OPEN_SUCCEEDED;
std::move(callback).Run(success);
},
std::move(callback)));
}
void HoldingSpaceClientImpl::OpenItems(
const std::vector<const HoldingSpaceItem*>& items,
holding_space_metrics::EventSource event_source,
SuccessCallback callback) {
holding_space_metrics::RecordItemAction(
items, holding_space_metrics::ItemAction::kLaunch, event_source);
if (items.empty()) {
std::move(callback).Run(/*success=*/false);
return;
}
auto complete_success = std::make_unique<bool>(true);
auto* complete_success_ptr = complete_success.get();
base::RepeatingClosure barrier_closure = base::BarrierClosure(
items.size(),
base::BindOnce(
[](std::unique_ptr<bool> complete_success, SuccessCallback callback) {
std::move(callback).Run(*complete_success);
},
std::move(complete_success), std::move(callback)));
for (const HoldingSpaceItem* item : items) {
if (item->file().file_path.empty()) {
holding_space_metrics::RecordItemLaunchFailure(
item->type(), item->file().file_path,
ItemLaunchFailureReason::kPathEmpty);
*complete_success_ptr = false;
barrier_closure.Run();
continue;
}
if (!item->progress().IsComplete()) {
const std::optional<ItemLaunchFailureReason> failure_to_launch_reason =
OpenInProgressItem(profile_, *item, event_source);
if (failure_to_launch_reason) {
holding_space_metrics::RecordItemLaunchFailure(
item->type(), item->file().file_path,
failure_to_launch_reason.value());
}
*complete_success_ptr &= !failure_to_launch_reason.has_value();
barrier_closure.Run();
continue;
}
GetFileInfo(
profile_, item->file().file_path,
base::BindOnce(
[](const base::WeakPtr<HoldingSpaceClientImpl>& weak_ptr,
base::RepeatingClosure barrier_closure, bool* complete_success,
const base::FilePath& file_path, HoldingSpaceItem::Type type,
const std::optional<base::File::Info>& info) {
if (!weak_ptr || !info.has_value()) {
holding_space_metrics::RecordItemLaunchFailure(
type, file_path,
weak_ptr ? ItemLaunchFailureReason::kFileInfoError
: ItemLaunchFailureReason::kShutdown);
*complete_success = false;
barrier_closure.Run();
return;
}
if (!info->size) {
holding_space_metrics::RecordItemLaunchEmpty(type, file_path);
}
file_manager::util::OpenItem(
weak_ptr->profile_, file_path,
info.value().is_directory ? platform_util::OPEN_FOLDER
: platform_util::OPEN_FILE,
base::BindOnce(
[](base::RepeatingClosure barrier_closure,
bool* complete_success, HoldingSpaceItem::Type type,
const base::FilePath& file_path,
platform_util::OpenOperationResult result) {
const bool success =
result == platform_util::OPEN_SUCCEEDED;
if (!success) {
holding_space_metrics::RecordItemLaunchFailure(
type, file_path,
ToItemLaunchFailureReason(result).value());
*complete_success = false;
}
barrier_closure.Run();
},
barrier_closure, complete_success, type, file_path));
},
weak_factory_.GetWeakPtr(), barrier_closure, complete_success_ptr,
item->file().file_path, item->type()));
}
}
void HoldingSpaceClientImpl::OpenMyFiles(SuccessCallback callback) {
auto file_path = file_manager::util::GetMyFilesFolderForProfile(profile_);
if (file_path.empty()) {
std::move(callback).Run(/*success=*/false);
return;
}
file_manager::util::OpenItem(
profile_, file_path, platform_util::OPEN_FOLDER,
base::BindOnce(
[](SuccessCallback callback,
platform_util::OpenOperationResult result) {
const bool success = result == platform_util::OPEN_SUCCEEDED;
std::move(callback).Run(success);
},
std::move(callback)));
}
void HoldingSpaceClientImpl::PinFiles(
const std::vector<base::FilePath>& file_paths,
holding_space_metrics::EventSource event_source) {
std::vector<storage::FileSystemURL> file_system_urls;
for (const base::FilePath& file_path : file_paths) {
const GURL crack_url =
holding_space_util::ResolveFileSystemUrl(profile_, file_path);
const storage::FileSystemURL& file_system_url =
file_manager::util::GetFileManagerFileSystemContext(profile_)
->CrackURLInFirstPartyContext(crack_url);
file_system_urls.push_back(file_system_url);
}
if (!file_system_urls.empty()) {
GetHoldingSpaceKeyedService(profile_)->AddPinnedFiles(file_system_urls,
event_source);
}
}
void HoldingSpaceClientImpl::PinItems(
const std::vector<const HoldingSpaceItem*>& items,
holding_space_metrics::EventSource event_source) {
std::vector<storage::FileSystemURL> file_system_urls;
// NOTE: In-progress holding space items are neither pin- nor unpin-able.
HoldingSpaceKeyedService* service = GetHoldingSpaceKeyedService(profile_);
for (const HoldingSpaceItem* item : items) {
if (!item->progress().IsComplete()) {
continue;
}
const GURL& crack_url = item->file().file_system_url;
const storage::FileSystemURL& file_system_url =
file_manager::util::GetFileManagerFileSystemContext(profile_)
->CrackURLInFirstPartyContext(crack_url);
if (!service->ContainsPinnedFile(file_system_url)) {
file_system_urls.push_back(file_system_url);
}
}
if (!file_system_urls.empty()) {
service->AddPinnedFiles(file_system_urls, event_source);
}
}
void HoldingSpaceClientImpl::RefreshSuggestions() {
GetHoldingSpaceKeyedService(profile_)->RefreshSuggestions();
}
void HoldingSpaceClientImpl::RemoveSuggestions(
const std::vector<base::FilePath>& absolute_file_paths) {
GetHoldingSpaceKeyedService(profile_)->RemoveSuggestions(absolute_file_paths);
}
void HoldingSpaceClientImpl::ShowItemInFolder(
const HoldingSpaceItem& item,
holding_space_metrics::EventSource event_source,
SuccessCallback callback) {
holding_space_metrics::RecordItemAction(
{&item}, holding_space_metrics::ItemAction::kShowInFolder, event_source);
if (item.file().file_path.empty()) {
std::move(callback).Run(/*success=*/false);
return;
}
file_manager::util::ShowItemInFolder(
profile_, item.file().file_path,
base::BindOnce(
[](SuccessCallback callback,
platform_util::OpenOperationResult result) {
const bool success = result == platform_util::OPEN_SUCCEEDED;
std::move(callback).Run(success);
},
std::move(callback)));
}
void HoldingSpaceClientImpl::UnpinItems(
const std::vector<const HoldingSpaceItem*>& items,
holding_space_metrics::EventSource event_source) {
std::vector<storage::FileSystemURL> file_system_urls;
// NOTE: In-progress holding space items are neither pin- nor unpin-able.
HoldingSpaceKeyedService* service = GetHoldingSpaceKeyedService(profile_);
for (const HoldingSpaceItem* item : items) {
if (!item->progress().IsComplete()) {
continue;
}
const GURL& crack_url = item->file().file_system_url;
const storage::FileSystemURL& file_system_url =
file_manager::util::GetFileManagerFileSystemContext(profile_)
->CrackURLInFirstPartyContext(crack_url);
if (service->ContainsPinnedFile(file_system_url)) {
file_system_urls.push_back(file_system_url);
}
}
if (!file_system_urls.empty()) {
service->RemovePinnedFiles(file_system_urls, event_source);
}
}
} // namespace ash