blob: b6021c9de2ad3886ac3f2903791caf4b4302bcd6 [file] [log] [blame]
// Copyright 2020 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/ash/holding_space/holding_space_thumbnail_loader.h"
#include "ash/public/cpp/image_downloader.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/optional.h"
#include "base/single_thread_task_runner.h"
#include "base/util/values/values_util.h"
#include "base/values.h"
#include "chrome/browser/bitmap_fetcher/bitmap_fetcher.h"
#include "chrome/browser/bitmap_fetcher/bitmap_fetcher_delegate.h"
#include "chrome/browser/chromeos/file_manager/app_id.h"
#include "chrome/browser/chromeos/file_manager/fileapi_util.h"
#include "chrome/browser/extensions/api/messaging/native_message_port.h"
#include "chrome/browser/profiles/profile.h"
#include "extensions/browser/api/messaging/channel_endpoint.h"
#include "extensions/browser/api/messaging/message_service.h"
#include "extensions/browser/api/messaging/native_message_host.h"
#include "extensions/common/api/messaging/messaging_endpoint.h"
#include "extensions/common/api/messaging/port_id.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "storage/browser/file_system/file_system_context.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/skia_util.h"
#include "url/gurl.h"
namespace ash {
namespace {
// The native host name that will identify the holding space thumbnail loader to
// the image loader extension.
constexpr char kNativeMessageHostName[] =
"com.google.holding_space_thumbnail_loader";
using ThumbnailDataCallback = base::OnceCallback<void(const std::string& data)>;
// Handles a parsed message sent from image loader extension in response to a
// thumbnail request.
void HandleParsedThumbnailResponse(
const std::string& request_id,
ThumbnailDataCallback callback,
data_decoder::DataDecoder::ValueOrError result) {
if (!result.value) {
VLOG(2) << "Failed to parse request response " << *result.error;
std::move(callback).Run("");
return;
}
if (!result.value->is_dict()) {
VLOG(2) << "Invalid response format";
std::move(callback).Run("");
return;
}
const std::string* received_request_id =
result.value->FindStringKey("taskId");
const std::string* data = result.value->FindStringKey("data");
if (!data || !received_request_id || *received_request_id != request_id) {
std::move(callback).Run("");
return;
}
std::move(callback).Run(*data);
}
// Native message host for communication to the image loader extension.
// It handles a single image request - when the connection to the extension is
// established, it send a message containing an image request to the image
// loader. It closes the connection once it receives a response from the image
// loader.
class ThumbnailLoaderNativeMessageHost : public extensions::NativeMessageHost {
public:
ThumbnailLoaderNativeMessageHost(const std::string& request_id,
const std::string& message,
ThumbnailDataCallback callback)
: request_id_(request_id),
message_(message),
callback_(std::move(callback)) {}
~ThumbnailLoaderNativeMessageHost() override {
if (callback_)
std::move(callback_).Run("");
}
void OnMessage(const std::string& message) override {
if (response_received_)
return;
response_received_ = true;
// Detach the callback from the message host in case the extension closes
// connection by the time the response is parsed.
data_decoder::DataDecoder::ParseJsonIsolated(
message, base::BindOnce(&HandleParsedThumbnailResponse, request_id_,
std::move(callback_)));
client_->CloseChannel("");
client_ = nullptr;
}
void Start(Client* client) override {
client_ = client;
client_->PostMessageFromNativeHost(message_);
}
scoped_refptr<base::SingleThreadTaskRunner> task_runner() const override {
return task_runner_;
}
private:
const std::string request_id_;
const std::string message_;
ThumbnailDataCallback callback_;
Client* client_ = nullptr;
bool response_received_ = false;
const scoped_refptr<base::SingleThreadTaskRunner> task_runner_ =
base::ThreadTaskRunnerHandle::Get();
};
} // namespace
// Converts a data URL to bitmap.
class HoldingSpaceThumbnailLoader::ThumbnailDecoder
: public BitmapFetcherDelegate {
public:
explicit ThumbnailDecoder(Profile* profile) : profile_(profile) {}
ThumbnailDecoder(const ThumbnailDecoder&) = delete;
ThumbnailDecoder& operator=(const ThumbnailDecoder&) = delete;
~ThumbnailDecoder() override = default;
// BitmapFetcherDelegate:
void OnFetchComplete(const GURL& url, const SkBitmap* bitmap) override {
std::move(callback_).Run(bitmap, base::File::FILE_OK);
}
void Start(const std::string& data,
HoldingSpaceThumbnailLoader::ImageCallback callback) {
DCHECK(!callback_);
DCHECK(!bitmap_fetcher_);
// The data sent from the image loader extension should be in form of a data
// URL.
GURL data_url(data);
if (!data_url.is_valid() || !data_url.SchemeIs(url::kDataScheme)) {
std::move(callback).Run(/*bitmap=*/nullptr,
base::File::FILE_ERROR_FAILED);
return;
}
callback_ = std::move(callback);
// Note that the image downloader will not use network traffic for data
// URLs.
bitmap_fetcher_ = std::make_unique<BitmapFetcher>(
data_url, this, MISSING_TRAFFIC_ANNOTATION);
bitmap_fetcher_->Init(
/*referrer=*/std::string(), net::ReferrerPolicy::NEVER_CLEAR,
network::mojom::CredentialsMode::kOmit);
bitmap_fetcher_->Start(profile_->GetURLLoaderFactory().get());
}
private:
Profile* const profile_;
HoldingSpaceThumbnailLoader::ImageCallback callback_;
std::unique_ptr<BitmapFetcher> bitmap_fetcher_;
};
HoldingSpaceThumbnailLoader::HoldingSpaceThumbnailLoader(Profile* profile)
: profile_(profile) {}
HoldingSpaceThumbnailLoader::~HoldingSpaceThumbnailLoader() = default;
HoldingSpaceThumbnailLoader::ThumbnailRequest::ThumbnailRequest(
const base::FilePath& item_path,
const gfx::Size& size)
: item_path(item_path), size(size) {}
HoldingSpaceThumbnailLoader::ThumbnailRequest::~ThumbnailRequest() = default;
base::WeakPtr<HoldingSpaceThumbnailLoader>
HoldingSpaceThumbnailLoader::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void HoldingSpaceThumbnailLoader::Load(const ThumbnailRequest& request,
ImageCallback callback) {
// Get the item's last modified time - this will be used for cache lookup in
// the image loader extension.
file_manager::util::GetMetadataForPath(
file_manager::util::GetFileSystemContextForExtensionId(
profile_, file_manager::kImageLoaderExtensionId),
request.item_path,
storage::FileSystemOperation::GET_METADATA_FIELD_IS_DIRECTORY |
storage::FileSystemOperation::GET_METADATA_FIELD_LAST_MODIFIED,
base::BindOnce(&HoldingSpaceThumbnailLoader::LoadForFileWithMetadata,
weak_factory_.GetWeakPtr(), request, std::move(callback)));
}
void HoldingSpaceThumbnailLoader::LoadForFileWithMetadata(
const ThumbnailRequest& request,
ImageCallback callback,
base::File::Error result,
const base::File::Info& file_info) {
if (result != base::File::FILE_OK) {
std::move(callback).Run(/*bitmap=*/nullptr, result);
return;
}
// Short-circuit icons for folders.
if (file_info.is_directory) {
// `FILE_ERROR_NOT_A_FILE` is a special value used to signify that the
// file for which the thumbnail was requested is actually a folder.
std::move(callback).Run(/*bitmap=*/nullptr,
base::File::FILE_ERROR_NOT_A_FILE);
return;
}
GURL thumbnail_url;
if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
profile_, request.item_path, file_manager::kImageLoaderExtensionId,
&thumbnail_url)) {
std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED);
return;
}
extensions::MessageService* const message_service =
extensions::MessageService::Get(profile_);
if (!message_service) { // May be `nullptr` in tests.
std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED);
return;
}
base::UnguessableToken request_id = base::UnguessableToken::Create();
requests_[request_id] = std::move(callback);
// Unfortunately the image loader only supports cropping to square dimensions
// but a request for a non-cropped, non-square image would result in image
// distortion. To work around this we always request square images and then
// crop to requested dimensions on our end if necessary after bitmap decoding.
const int size = std::max(request.size.width(), request.size.height());
// Generate an image loader request. The request type is defined in
// ui/file_manager/image_loader/load_image_request.js.
base::Value request_value(base::Value::Type::DICTIONARY);
request_value.SetKey("taskId", base::Value(request_id.ToString()));
request_value.SetKey("url", base::Value(thumbnail_url.spec()));
request_value.SetKey("timestamp", util::TimeToValue(file_info.last_modified));
request_value.SetBoolKey("cache", true);
request_value.SetBoolKey("crop", true);
request_value.SetKey("priority", base::Value(1));
request_value.SetKey("width", base::Value(size));
request_value.SetKey("height", base::Value(size));
std::string request_message;
base::JSONWriter::Write(request_value, &request_message);
// Open a channel to the image loader extension using a message host that send
// the image loader request.
auto native_message_host = std::make_unique<ThumbnailLoaderNativeMessageHost>(
request_id.ToString(), request_message,
base::BindOnce(&HoldingSpaceThumbnailLoader::OnThumbnailLoaded,
weak_factory_.GetWeakPtr(), request_id, request.size));
const extensions::PortId port_id(base::UnguessableToken::Create(),
1 /* port_number */, true /* is_opener */);
auto native_message_port = std::make_unique<extensions::NativeMessagePort>(
message_service->GetChannelDelegate(), port_id,
std::move(native_message_host));
message_service->OpenChannelToExtension(
extensions::ChannelEndpoint(profile_), port_id,
extensions::MessagingEndpoint::ForNativeApp(kNativeMessageHostName),
std::move(native_message_port), file_manager::kImageLoaderExtensionId,
GURL(), std::string() /* channel_name */);
}
void HoldingSpaceThumbnailLoader::OnThumbnailLoaded(
const base::UnguessableToken& request_id,
const gfx::Size& requested_size,
const std::string& data) {
if (!requests_.count(request_id))
return;
if (data.empty()) {
RespondToRequest(request_id, requested_size, /*bitmap=*/nullptr,
base::File::FILE_ERROR_FAILED);
return;
}
auto thumbnail_decoder = std::make_unique<ThumbnailDecoder>(profile_);
ThumbnailDecoder* thumbnail_decoder_ptr = thumbnail_decoder.get();
thumbnail_decoders_.emplace(request_id, std::move(thumbnail_decoder));
thumbnail_decoder_ptr->Start(
data,
base::BindOnce(&HoldingSpaceThumbnailLoader::RespondToRequest,
weak_factory_.GetWeakPtr(), request_id, requested_size));
}
void HoldingSpaceThumbnailLoader::RespondToRequest(
const base::UnguessableToken& request_id,
const gfx::Size& requested_size,
const SkBitmap* bitmap,
base::File::Error error) {
thumbnail_decoders_.erase(request_id);
auto request_it = requests_.find(request_id);
if (request_it == requests_.end())
return;
// To work around cropping limitations of the image loader, we requested a
// square image. If requested dimensions were non-square, we need to perform
// additional cropping on our end.
SkBitmap cropped_bitmap;
if (bitmap) {
gfx::Rect cropped_rect(0, 0, bitmap->width(), bitmap->height());
if (cropped_rect.size() != requested_size) {
cropped_bitmap = *bitmap;
cropped_rect.ClampToCenteredSize(requested_size);
bitmap->extractSubset(&cropped_bitmap, gfx::RectToSkIRect(cropped_rect));
}
}
ImageCallback callback = std::move(request_it->second);
requests_.erase(request_it);
std::move(callback).Run(cropped_bitmap.isNull() ? bitmap : &cropped_bitmap,
error);
}
} // namespace ash