[go: nahoru, domu]

blob: b3edcbc2a7acf5bc0f1d9181ed5e18e7334f64e4 [file] [log] [blame]
// 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/chromeos/arc/print_spooler/print_session_impl.h"
#include <limits>
#include <string>
#include <utility>
#include "ash/public/cpp/arc_custom_tab.h"
#include "base/bind.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/platform_file.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/numerics/safe_conversions.h"
#include "base/optional.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "chrome/browser/chromeos/arc/print_spooler/arc_print_spooler_util.h"
#include "chrome/browser/printing/print_view_manager_common.h"
#include "chrome/browser/printing/printing_service.h"
#include "components/arc/mojom/print_common.mojom.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/c/system/types.h"
#include "net/base/filename_util.h"
#include "printing/mojom/print.mojom.h"
#include "printing/page_range.h"
#include "printing/print_job_constants.h"
#include "printing/print_settings.h"
#include "printing/print_settings_conversion.h"
#include "printing/units.h"
#include "ui/aura/window.h"
#include "ui/gfx/geometry/size.h"
namespace arc {
namespace {
constexpr int kMinimumPdfSize = 50;
// Converts a color mode to its Mojo type.
mojom::PrintColorMode ToArcColorMode(int color_mode) {
base::Optional<bool> is_color = printing::IsColorModelSelected(color_mode);
return is_color.value() ? mojom::PrintColorMode::COLOR
: mojom::PrintColorMode::MONOCHROME;
}
// Converts a duplex mode to its Mojo type.
mojom::PrintDuplexMode ToArcDuplexMode(int duplex_mode) {
printing::mojom::DuplexMode mode =
static_cast<printing::mojom::DuplexMode>(duplex_mode);
switch (mode) {
case printing::mojom::DuplexMode::kLongEdge:
return mojom::PrintDuplexMode::LONG_EDGE;
case printing::mojom::DuplexMode::kShortEdge:
return mojom::PrintDuplexMode::SHORT_EDGE;
default:
return mojom::PrintDuplexMode::NONE;
}
}
// Gets and builds the print attributes from the job settings.
mojom::PrintAttributesPtr GetPrintAttributes(const base::Value& job_settings) {
// PrintMediaSize:
const base::Value* media_size_value =
job_settings.FindDictKey(printing::kSettingMediaSize);
if (!media_size_value)
return nullptr;
// Vendor ID will be empty when Destination is Save as PDF.
const std::string* vendor_id =
media_size_value->FindStringKey(printing::kSettingMediaSizeVendorId);
std::string id = "PDF";
if (vendor_id && !vendor_id->empty()) {
id = *vendor_id;
}
base::Optional<int> width_microns =
media_size_value->FindIntKey(printing::kSettingMediaSizeWidthMicrons);
base::Optional<int> height_microns =
media_size_value->FindIntKey(printing::kSettingMediaSizeHeightMicrons);
if (!width_microns.has_value() || !height_microns.has_value())
return nullptr;
// Swap the width and height if layout is landscape.
base::Optional<bool> landscape =
job_settings.FindBoolKey(printing::kSettingLandscape);
if (!landscape.has_value())
return nullptr;
gfx::Size size_micron;
if (landscape.value()) {
size_micron = gfx::Size(height_microns.value(), width_microns.value());
} else {
size_micron = gfx::Size(width_microns.value(), height_microns.value());
}
gfx::Size size_mil =
gfx::ScaleToRoundedSize(size_micron, 1.0f / printing::kMicronsPerMil);
mojom::PrintMediaSizePtr media_size = mojom::PrintMediaSize::New(
id, "ARC", size_mil.width(), size_mil.height());
// PrintResolution:
int horizontal_dpi = job_settings.FindIntKey(printing::kSettingDpiHorizontal)
.value_or(printing::kDefaultPdfDpi);
int vertical_dpi = job_settings.FindIntKey(printing::kSettingDpiVertical)
.value_or(printing::kDefaultPdfDpi);
// PrintMargins:
// Chrome uses margins to fit content to the printable area. Android uses
// margins to crop content to the printable area. Set margins to 0 to prevent
// cropping.
mojom::PrintMarginsPtr margins = mojom::PrintMargins::New(0, 0, 0, 0);
// PrintColorMode:
base::Optional<int> color = job_settings.FindIntKey(printing::kSettingColor);
if (!color.has_value())
return nullptr;
mojom::PrintColorMode color_mode = ToArcColorMode(color.value());
// PrintDuplexMode:
base::Optional<int> duplex =
job_settings.FindIntKey(printing::kSettingDuplexMode);
if (!duplex.has_value())
return nullptr;
mojom::PrintDuplexMode duplex_mode = ToArcDuplexMode(duplex.value());
return mojom::PrintAttributes::New(
std::move(media_size), gfx::Size(horizontal_dpi, vertical_dpi),
std::move(margins), color_mode, duplex_mode);
}
// Creates a PrintDocumentRequest from the provided |job_settings|. Uses helper
// functions to parse |job_settings|.
mojom::PrintDocumentRequestPtr PrintDocumentRequestFromJobSettings(
const base::Value& job_settings) {
return mojom::PrintDocumentRequest::New(
printing::GetPageRangesFromJobSettings(job_settings),
GetPrintAttributes(job_settings));
}
// Uses the provided ScopedHandle to read a preview document from ARC into
// read-only shared memory.
base::ReadOnlySharedMemoryRegion ReadPreviewDocument(
mojo::ScopedHandle preview_document,
size_t data_size) {
base::PlatformFile platform_file;
if (mojo::UnwrapPlatformFile(std::move(preview_document), &platform_file) !=
MOJO_RESULT_OK) {
return base::ReadOnlySharedMemoryRegion();
}
base::File src_file(platform_file);
if (!src_file.IsValid()) {
DPLOG(ERROR) << "Source file is invalid.";
return base::ReadOnlySharedMemoryRegion();
}
base::MappedReadOnlyRegion region_mapping =
base::ReadOnlySharedMemoryRegion::Create(data_size);
if (!region_mapping.IsValid())
return std::move(region_mapping.region);
bool success = src_file.ReadAndCheck(
0, region_mapping.mapping.GetMemoryAsSpan<uint8_t>());
if (!success) {
DPLOG(ERROR) << "Error reading PDF.";
return base::ReadOnlySharedMemoryRegion();
}
return std::move(region_mapping.region);
}
// Checks if |web_contents| contains a subframe with a Chrome extension URL
// that claims to be done loading. This isn't foolproof, but it ensures that
// print preview will find the embedded plugin element instead of trying to
// print the top-level frame.
bool IsPdfPluginLoaded(content::WebContents* web_contents) {
if (!web_contents->IsDocumentOnLoadCompletedInMainFrame()) {
VLOG(1) << "Top-level WebContents not ready yet.";
return false;
}
content::WebContents* contents_to_use =
printing::GetWebContentsToUse(web_contents);
if (contents_to_use == web_contents) {
VLOG(1) << "No plugin WebContents found yet.";
return false;
}
GURL url = contents_to_use->GetMainFrame()->GetLastCommittedURL();
if (!url.SchemeIs("chrome-extension")) {
VLOG(1) << "Plugin frame URL not loaded yet.";
return false;
}
if (!contents_to_use->IsDocumentOnLoadCompletedInMainFrame()) {
VLOG(1) << "Plugin frame still loading.";
return false;
}
VLOG(1) << "PDF plugin has loaded.";
return true;
}
} // namespace
// static
mojo::PendingRemote<mojom::PrintSessionHost> PrintSessionImpl::Create(
std::unique_ptr<content::WebContents> web_contents,
std::unique_ptr<ash::ArcCustomTab> custom_tab,
mojom::PrintSessionInstancePtr instance) {
if (!custom_tab || !instance)
return mojo::NullRemote();
// This object will be deleted when the mojo connection is closed.
mojo::PendingRemote<mojom::PrintSessionHost> remote;
new PrintSessionImpl(std::move(web_contents), std::move(custom_tab),
std::move(instance),
remote.InitWithNewPipeAndPassReceiver());
return remote;
}
PrintSessionImpl::PrintSessionImpl(
std::unique_ptr<content::WebContents> web_contents,
std::unique_ptr<ash::ArcCustomTab> custom_tab,
mojom::PrintSessionInstancePtr instance,
mojo::PendingReceiver<mojom::PrintSessionHost> receiver)
: ArcCustomTabModalDialogHost(std::move(custom_tab), web_contents.get()),
instance_(std::move(instance)),
session_receiver_(this, std::move(receiver)),
web_contents_(std::move(web_contents)) {
session_receiver_.set_disconnect_handler(
base::BindOnce(&PrintSessionImpl::Close, weak_ptr_factory_.GetWeakPtr()));
web_contents_->SetUserData(UserDataKey(), base::WrapUnique(this));
aura::Window* window = web_contents_->GetNativeView();
custom_tab_->Attach(window);
window->Show();
// TODO(jschettler): Handle this correctly once crbug.com/636642 is
// resolved. Until then, give the PDF plugin time to load.
VLOG(1) << "Waiting for PDF plugin to load.";
StartPrintAfterPluginIsLoaded();
}
PrintSessionImpl::~PrintSessionImpl() {
// Delete the saved print document now that it's no longer needed.
base::FilePath file_path;
if (!net::FileURLToFilePath(web_contents_->GetVisibleURL(), &file_path)) {
LOG(ERROR) << "Failed to obtain file path from URL.";
return;
}
base::ThreadPool::PostTask(FROM_HERE, {base::MayBlock()},
base::BindOnce(&DeletePrintDocument, file_path));
}
void PrintSessionImpl::CreatePreviewDocument(
base::Value job_settings,
CreatePreviewDocumentCallback callback) {
mojom::PrintDocumentRequestPtr request =
PrintDocumentRequestFromJobSettings(job_settings);
if (!request || !request->attributes) {
std::move(callback).Run(base::ReadOnlySharedMemoryRegion());
return;
}
int request_id = job_settings.FindIntKey(printing::kPreviewRequestID).value();
instance_->CreatePreviewDocument(
std::move(request),
base::BindOnce(&PrintSessionImpl::OnPreviewDocumentCreated,
weak_ptr_factory_.GetWeakPtr(), request_id,
std::move(callback)));
}
void PrintSessionImpl::OnPreviewDocumentCreated(
int request_id,
CreatePreviewDocumentCallback callback,
mojo::ScopedHandle preview_document,
int64_t data_size) {
if (data_size < kMinimumPdfSize ||
!base::IsValueInRangeForNumericType<size_t>(data_size)) {
std::move(callback).Run(base::ReadOnlySharedMemoryRegion());
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&ReadPreviewDocument, std::move(preview_document),
static_cast<size_t>(data_size)),
base::BindOnce(&PrintSessionImpl::OnPreviewDocumentRead,
weak_ptr_factory_.GetWeakPtr(), request_id,
std::move(callback)));
}
void PrintSessionImpl::OnPreviewDocumentRead(
int request_id,
CreatePreviewDocumentCallback callback,
base::ReadOnlySharedMemoryRegion preview_document_region) {
if (!preview_document_region.IsValid()) {
std::move(callback).Run(std::move(preview_document_region));
return;
}
if (!pdf_flattener_.is_bound()) {
GetPrintingService()->BindPdfFlattener(
pdf_flattener_.BindNewPipeAndPassReceiver());
pdf_flattener_.set_disconnect_handler(
base::BindOnce(&PrintSessionImpl::OnPdfFlattenerDisconnected,
weak_ptr_factory_.GetWeakPtr()));
}
bool inserted = callbacks_.emplace(request_id, std::move(callback)).second;
DCHECK(inserted);
pdf_flattener_->FlattenPdf(
std::move(preview_document_region),
base::BindOnce(&PrintSessionImpl::OnPdfFlattened,
weak_ptr_factory_.GetWeakPtr(), request_id));
}
void PrintSessionImpl::OnPdfFlattened(
int request_id,
base::ReadOnlySharedMemoryRegion flattened_document_region) {
auto it = callbacks_.find(request_id);
std::move(it->second).Run(std::move(flattened_document_region));
callbacks_.erase(it);
}
void PrintSessionImpl::OnPdfFlattenerDisconnected() {
for (auto& it : callbacks_)
std::move(it.second).Run(base::ReadOnlySharedMemoryRegion());
callbacks_.clear();
}
void PrintSessionImpl::Close() {
web_contents_->RemoveUserData(UserDataKey());
}
void PrintSessionImpl::OnPrintPreviewClosed() {
instance_->OnPrintPreviewClosed();
}
void PrintSessionImpl::StartPrintAfterPluginIsLoaded() {
// We know that we loaded a PDF into our WebContents. It takes some time for
// the PDF plugin to load and create its document structure. If StartPrint()
// is called too soon, it won't find this structure and will attach to the
// top-level frame instead of the correct PDF element. The PDF plugin doesn't
// have a way to notify the browser when it's ready (crbug.com/636642), so we
// need to poll for the PDF frame to "look ready" before we start printing.
if (!IsPdfPluginLoaded(web_contents_.get())) {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&PrintSessionImpl::StartPrintAfterPluginIsLoaded,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(100));
LOG(WARNING) << "PDF plugin not ready yet. Can't start print preview.";
return;
}
// The inner doc has been marked done, but the PDF plugin might not be quite
// done updating the DOM yet. We don't have a way to check that, so launch
// printing after one final delay to give that time to finish.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&PrintSessionImpl::StartPrintNow,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(100));
}
void PrintSessionImpl::StartPrintNow() {
printing::StartPrint(web_contents_.get(),
print_renderer_receiver_.BindNewEndpointAndPassRemote(),
false, false);
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(PrintSessionImpl)
} // namespace arc