blob: 17e9f27fd42b222ed784e793ca46787b22e1cdc2 [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/ash/scanning/scan_service.h"
#include <cstdint>
#include <utility>
#include "ash/constants/ash_features.h"
#include "ash/webui/scanning/scanning_uma.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/task_runner_util.h"
#include "base/time/time.h"
#include "chrome/browser/ash/scanning/lorgnette_scanner_manager.h"
#include "chrome/browser/ash/scanning/scanning_file_path_helper.h"
#include "chrome/browser/ash/scanning/scanning_type_converters.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 "chromeos/utils/pdf_conversion.h"
#include "mojo/public/cpp/bindings/enum_traits.h"
#include "mojo/public/cpp/bindings/struct_traits.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace ash {
namespace {
namespace mojo_ipc = scanning::mojom;
// The max progress percent that can be reported for a scanned page.
constexpr uint32_t kMaxProgressPercent = 100;
// Creates a filename for a scanned image using |start_time|, |page_number|, and
// |file_ext|.
std::string CreateFilename(const base::Time::Exploded& start_time,
uint32_t page_number,
const mojo_ipc::FileType file_type) {
std::string file_ext;
switch (file_type) {
case mojo_ipc::FileType::kSearchablePdf:
DCHECK(base::FeatureList::IsEnabled(
chromeos::features::kScanAppSearchablePdf));
// Temporarily set searchable pdfs to follow png pipeline while
// implementing.
FALLTHROUGH;
case mojo_ipc::FileType::kPng:
file_ext = "png";
break;
case mojo_ipc::FileType::kJpg:
file_ext = "jpg";
break;
case mojo_ipc::FileType::kPdf:
// The filename of a PDF doesn't include the page number.
return base::StringPrintf("scan_%02d%02d%02d-%02d%02d%02d.pdf",
start_time.year, start_time.month,
start_time.day_of_month, start_time.hour,
start_time.minute, start_time.second);
}
return base::StringPrintf(
"scan_%02d%02d%02d-%02d%02d%02d_%d.%s", start_time.year, start_time.month,
start_time.day_of_month, start_time.hour, start_time.minute,
start_time.second, page_number, file_ext.c_str());
}
// Helper function that writes |scanned_image| to |file_path|.
// Returns whether the image was successfully written.
bool WriteImage(const base::FilePath& file_path,
const std::string& scanned_image) {
if (!base::WriteFile(file_path, scanned_image)) {
LOG(ERROR) << "Failed to save scanned image: " << file_path.value().c_str();
return false;
}
return true;
}
// Adds |jpg_images| to a single PDF, and writes the PDF to |file_path|. If
// |rotate_alternate_pages| is true, every other page is rotated 180 degrees.
// Returns whether the PDF was successfully saved.
bool SaveAsPdf(const std::vector<std::string>& jpg_images,
const base::FilePath& file_path,
bool rotate_alternate_pages) {
return chromeos::ConvertJpgImagesToPdf(jpg_images, file_path,
rotate_alternate_pages);
}
// Saves |scanned_image| to a file after converting it if necessary. Returns the
// file path to the saved file if the save succeeds.
base::FilePath SavePage(const base::FilePath& scan_to_path,
const mojo_ipc::FileType file_type,
std::string scanned_image,
uint32_t page_number,
const base::Time::Exploded& start_time) {
std::string filename = CreateFilename(start_time, page_number, file_type);
if (!WriteImage(scan_to_path.Append(filename), scanned_image))
return base::FilePath();
return scan_to_path.Append(filename);
}
// Returns a ScanJobFailureReason corresponding to the given |failure_mode|.
scanning::ScanJobFailureReason GetScanJobFailureReason(
const lorgnette::ScanFailureMode failure_mode) {
switch (failure_mode) {
case lorgnette::SCAN_FAILURE_MODE_UNKNOWN:
return scanning::ScanJobFailureReason::kUnknownScannerError;
case lorgnette::SCAN_FAILURE_MODE_DEVICE_BUSY:
return scanning::ScanJobFailureReason::kDeviceBusy;
case lorgnette::SCAN_FAILURE_MODE_ADF_JAMMED:
return scanning::ScanJobFailureReason::kAdfJammed;
case lorgnette::SCAN_FAILURE_MODE_ADF_EMPTY:
return scanning::ScanJobFailureReason::kAdfEmpty;
case lorgnette::SCAN_FAILURE_MODE_FLATBED_OPEN:
return scanning::ScanJobFailureReason::kFlatbedOpen;
case lorgnette::SCAN_FAILURE_MODE_IO_ERROR:
return scanning::ScanJobFailureReason::kIoError;
case lorgnette::SCAN_FAILURE_MODE_NO_FAILURE:
case lorgnette::ScanFailureMode_INT_MIN_SENTINEL_DO_NOT_USE_:
case lorgnette::ScanFailureMode_INT_MAX_SENTINEL_DO_NOT_USE_:
NOTREACHED();
return scanning::ScanJobFailureReason::kUnknownScannerError;
}
}
// Records the histograms based on the scan job result.
void RecordScanJobResult(
bool success,
const absl::optional<scanning::ScanJobFailureReason>& failure_reason,
int num_files_created,
int num_pages_scanned) {
base::UmaHistogramBoolean("Scanning.ScanJobSuccessful", success);
if (success) {
base::UmaHistogramCounts100("Scanning.NumFilesCreated", num_files_created);
base::UmaHistogramCounts100("Scanning.NumPagesScanned", num_pages_scanned);
return;
}
if (failure_reason.has_value()) {
base::UmaHistogramEnumeration("Scanning.ScanJobFailureReason",
failure_reason.value());
}
}
} // namespace
ScanService::ScanService(LorgnetteScannerManager* lorgnette_scanner_manager,
base::FilePath my_files_path,
base::FilePath google_drive_path,
content::BrowserContext* context)
: lorgnette_scanner_manager_(lorgnette_scanner_manager),
context_(context),
task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})),
file_path_helper_(std::move(google_drive_path),
std::move(my_files_path)) {
DCHECK(lorgnette_scanner_manager_);
DCHECK(context_);
}
ScanService::~ScanService() = default;
void ScanService::GetScanners(GetScannersCallback callback) {
get_scanners_time_ = base::TimeTicks::Now();
lorgnette_scanner_manager_->GetScannerNames(
base::BindOnce(&ScanService::OnScannerNamesReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ScanService::GetScannerCapabilities(
const base::UnguessableToken& scanner_id,
GetScannerCapabilitiesCallback callback) {
const std::string scanner_name = GetScannerName(scanner_id);
if (scanner_name.empty()) {
std::move(callback).Run(mojo_ipc::ScannerCapabilities::New());
return;
}
lorgnette_scanner_manager_->GetScannerCapabilities(
scanner_name,
base::BindOnce(&ScanService::OnScannerCapabilitiesReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ScanService::StartScan(
const base::UnguessableToken& scanner_id,
mojo_ipc::ScanSettingsPtr settings,
mojo::PendingRemote<mojo_ipc::ScanJobObserver> observer,
StartScanCallback callback) {
ClearScanState();
scan_job_observer_.reset();
scan_job_observer_.Bind(std::move(observer));
// Unretained is safe here, because `this` owns `scan_job_observer_`, and no
// reply callbacks will be invoked once the mojo::Remote is destroyed.
scan_job_observer_.set_disconnect_handler(
base::BindOnce(&ScanService::CancelScan, base::Unretained(this)));
std::move(callback).Run(SendScanRequest(scanner_id, std::move(settings)));
}
bool ScanService::SendScanRequest(const base::UnguessableToken& scanner_id,
mojo_ipc::ScanSettingsPtr settings) {
const std::string scanner_name = GetScannerName(scanner_id);
if (scanner_name.empty()) {
RecordScanJobResult(false, scanning::ScanJobFailureReason::kScannerNotFound,
/*not used*/ 0, /*not used*/ 0);
return false;
}
if (!file_path_helper_.IsFilePathSupported(settings->scan_to_path)) {
RecordScanJobResult(false,
scanning::ScanJobFailureReason::kUnsupportedScanToPath,
/*not used*/ 0, /*not used*/ 0);
return false;
}
// Determine if an ADF scanner that flips alternate pages was selected.
rotate_alternate_pages_ = lorgnette_scanner_manager_->IsRotateAlternate(
scanner_name, settings->source_name);
base::Time::Now().LocalExplode(&start_time_);
lorgnette_scanner_manager_->Scan(
scanner_name,
mojo::StructTraits<lorgnette::ScanSettings,
mojo_ipc::ScanSettingsPtr>::ToMojom(settings),
base::BindRepeating(&ScanService::OnProgressPercentReceived,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&ScanService::OnPageReceived,
weak_ptr_factory_.GetWeakPtr(),
settings->scan_to_path, settings->file_type),
base::BindOnce(&ScanService::OnScanCompleted,
weak_ptr_factory_.GetWeakPtr()));
return true;
}
void ScanService::CancelScan() {
lorgnette_scanner_manager_->CancelScan(base::BindOnce(
&ScanService::OnCancelCompleted, weak_ptr_factory_.GetWeakPtr()));
}
void ScanService::BindInterface(
mojo::PendingReceiver<mojo_ipc::ScanService> pending_receiver) {
receiver_.Bind(std::move(pending_receiver));
}
void ScanService::Shutdown() {
lorgnette_scanner_manager_ = nullptr;
receiver_.reset();
weak_ptr_factory_.InvalidateWeakPtrs();
}
void ScanService::OnScannerNamesReceived(
GetScannersCallback callback,
std::vector<std::string> scanner_names) {
base::UmaHistogramCounts100("Scanning.NumDetectedScanners",
scanner_names.size());
scanner_names_.clear();
scanner_names_.reserve(scanner_names.size());
std::vector<mojo_ipc::ScannerPtr> scanners;
scanners.reserve(scanner_names.size());
for (const auto& name : scanner_names) {
base::UnguessableToken id = base::UnguessableToken::Create();
scanner_names_[id] = name;
scanners.push_back(mojo_ipc::Scanner::New(id, base::UTF8ToUTF16(name)));
}
std::move(callback).Run(std::move(scanners));
}
void ScanService::OnScannerCapabilitiesReceived(
GetScannerCapabilitiesCallback callback,
const absl::optional<lorgnette::ScannerCapabilities>& capabilities) {
if (!capabilities) {
LOG(ERROR) << "Failed to get scanner capabilities.";
std::move(callback).Run(mojo_ipc::ScannerCapabilities::New());
return;
}
// If this is the first time capabilities have been received since the last
// call to GetScanners(), record the time between the two events to capture
// the time between the Scan app launching and the user being able to interact
// with the app (e.g. select a scanner, change scan settings, or start a
// scan). If the user selects a different scanner and new capabilities are
// received, don't record the metric again.
if (!get_scanners_time_.is_null()) {
base::UmaHistogramMediumTimes("Scanning.ReadyTime",
base::TimeTicks::Now() - get_scanners_time_);
get_scanners_time_ = base::TimeTicks();
}
std::move(callback).Run(
mojo::StructTraits<
ash::scanning::mojom::ScannerCapabilitiesPtr,
lorgnette::ScannerCapabilities>::ToMojom(capabilities.value()));
}
void ScanService::OnProgressPercentReceived(uint32_t progress_percent,
uint32_t page_number) {
DCHECK_LE(progress_percent, kMaxProgressPercent);
scan_job_observer_->OnPageProgress(page_number, progress_percent);
}
void ScanService::OnPageReceived(const base::FilePath& scan_to_path,
const mojo_ipc::FileType file_type,
std::string scanned_image,
uint32_t page_number) {
// TODO(b/172670649): Update LorgnetteManagerClient to pass scan data as a
// vector.
// In case the last reported progress percent was less than 100, send one
// final progress event before the page complete event.
scan_job_observer_->OnPageProgress(page_number, kMaxProgressPercent);
scan_job_observer_->OnPageComplete(
std::vector<uint8_t>(scanned_image.begin(), scanned_image.end()));
++num_pages_scanned_;
// If the selected file type is PDF, the PDF will be created after all the
// scanned images are received.
if (file_type == mojo_ipc::FileType::kPdf) {
scanned_images_.push_back(std::move(scanned_image));
// The output of multi-page PDF scans is a single file so only create and
// append a single file path.
if (scanned_file_paths_.empty()) {
DCHECK_EQ(1, page_number);
scanned_file_paths_.push_back(scan_to_path.Append(CreateFilename(
start_time_, /*not used*/ 0, mojo_ipc::FileType::kPdf)));
}
return;
}
base::PostTaskAndReplyWithResult(
task_runner_.get(), FROM_HERE,
base::BindOnce(&SavePage, scan_to_path, file_type,
std::move(scanned_image), page_number, start_time_),
base::BindOnce(&ScanService::OnPageSaved,
weak_ptr_factory_.GetWeakPtr()));
}
void ScanService::OnScanCompleted(lorgnette::ScanFailureMode failure_mode) {
// |scanned_images_| only has data for PDF scans.
if (failure_mode == lorgnette::SCAN_FAILURE_MODE_NO_FAILURE &&
!scanned_images_.empty()) {
DCHECK(!scanned_file_paths_.empty());
base::PostTaskAndReplyWithResult(
task_runner_.get(), FROM_HERE,
base::BindOnce(&SaveAsPdf, scanned_images_, scanned_file_paths_.back(),
rotate_alternate_pages_),
base::BindOnce(&ScanService::OnPdfSaved,
weak_ptr_factory_.GetWeakPtr()));
}
// Post a task to the task runner to ensure all the pages have been saved
// before reporting the scan job as complete.
base::PostTaskAndReplyWithResult(
task_runner_.get(), FROM_HERE,
base::BindOnce(
[](lorgnette::ScanFailureMode failure_mode) { return failure_mode; },
failure_mode),
base::BindOnce(&ScanService::OnAllPagesSaved,
weak_ptr_factory_.GetWeakPtr()));
}
void ScanService::OnCancelCompleted(bool success) {
if (success)
ClearScanState();
scan_job_observer_->OnCancelComplete(success);
}
void ScanService::OnPdfSaved(const bool success) {
page_save_failed_ = !success;
}
void ScanService::OnPageSaved(const base::FilePath& saved_file_path) {
page_save_failed_ = page_save_failed_ || saved_file_path.empty();
if (page_save_failed_) {
return;
}
scanned_file_paths_.push_back(saved_file_path);
}
void ScanService::OnAllPagesSaved(lorgnette::ScanFailureMode failure_mode) {
absl::optional<scanning::ScanJobFailureReason> failure_reason = absl::nullopt;
if (failure_mode != lorgnette::SCAN_FAILURE_MODE_NO_FAILURE) {
failure_reason = GetScanJobFailureReason(failure_mode);
scanned_file_paths_.clear();
} else if (page_save_failed_) {
failure_mode = lorgnette::SCAN_FAILURE_MODE_UNKNOWN;
failure_reason = scanning::ScanJobFailureReason::kSaveToDiskFailed;
scanned_file_paths_.clear();
}
scan_job_observer_->OnScanComplete(
mojo::EnumTraits<ash::scanning::mojom::ScanResult,
lorgnette::ScanFailureMode>::ToMojom(failure_mode),
scanned_file_paths_);
HoldingSpaceKeyedService* holding_space_keyed_service =
HoldingSpaceKeyedServiceFactory::GetInstance()->GetService(context_);
if (holding_space_keyed_service) {
for (const auto& saved_scan_path : scanned_file_paths_)
holding_space_keyed_service->AddScan(saved_scan_path);
}
RecordScanJobResult(failure_mode == lorgnette::SCAN_FAILURE_MODE_NO_FAILURE &&
!page_save_failed_,
failure_reason, scanned_file_paths_.size(),
num_pages_scanned_);
}
void ScanService::ClearScanState() {
page_save_failed_ = false;
rotate_alternate_pages_ = false;
scanned_file_paths_.clear();
scanned_images_.clear();
num_pages_scanned_ = 0;
}
std::string ScanService::GetScannerName(
const base::UnguessableToken& scanner_id) {
const auto it = scanner_names_.find(scanner_id);
if (it == scanner_names_.end()) {
LOG(ERROR) << "Failed to find scanner name using the given scanner id.";
return "";
}
return it->second;
}
} // namespace ash