// 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/ash/scanning/scan_service.h"
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include "ash/constants/ash_features.h"
#include "ash/webui/scanning/mojom/scanning_type_converters.h"
#include "ash/webui/scanning/scanning_uma.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/time_formatting.h"
#include "base/location.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.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/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"
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;
// The number of minutes to wait before the scan times out and is canceled.
constexpr base::TimeDelta kTimeout = base::Minutes(15);
// Creates a filename for a scanned image using |start_time|, |page_number|, and
// |file_ext|.
std::string CreateFilename(const base::Time& start_time,
uint32_t page_number,
const mojo_ipc::FileType file_type) {
const std::string timestamp =
base::UnlocalizedTimeFormatWithPattern(start_time, "yyMMdd-HHmmss");
std::string file_ext;
switch (file_type) {
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_%s.pdf", timestamp.c_str());
}
return base::StringPrintf("scan_%s_%d.%s", timestamp.c_str(), 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,
bool is_multi_page_scan,
std::optional<int> dpi) {
size_t total_image_size = 0;
for (const std::string& image : jpg_images) {
total_image_size += image.size();
}
base::UmaHistogramCounts1M(
is_multi_page_scan
? "Scanning.MultiPageScan.CombinedImageSizeInKbBeforePdf"
: "Scanning.CombinedImageSizeInKbBeforePdf",
total_image_size / 1024);
const base::TimeTicks pdf_start_time = base::TimeTicks::Now();
const bool pdf_saved = chromeos::ConvertJpgImagesToPdf(
jpg_images, file_path, rotate_alternate_pages, dpi);
base::UmaHistogramTimes(is_multi_page_scan
? "Scanning.MultiPageScan.PDFGenerationTime"
: "Scanning.PDFGenerationTime",
base::TimeTicks::Now() - pdf_start_time);
return pdf_saved;
}
// 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& 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_IN_MIGRATION();
return scanning::ScanJobFailureReason::kUnknownScannerError;
}
}
// Records the histograms based on the scan job result.
void RecordScanJobResult(
bool success,
const std::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());
}
}
std::unique_ptr<device::PowerSaveBlocker> RequestWakeLock(
const std::string& description) {
return std::make_unique<device::PowerSaveBlocker>(
/*type=*/device::mojom::WakeLockType::kPreventDisplaySleepAllowDimming,
/*reason=*/device::mojom::WakeLockReason::kOther,
/*description=*/description,
/*ui_task_runner=*/base::SequencedTaskRunner::GetCurrentDefault(),
/*blocking_task_runner=*/nullptr);
}
} // 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();
SetScanJobObserver(std::move(observer));
wake_lock_ = RequestWakeLock("Scan Job in Progress");
std::move(callback).Run(SendScanRequest(
scanner_id, std::move(settings), /*page_index_to_replace=*/std::nullopt,
base::BindOnce(&ScanService::OnScanCompleted,
weak_ptr_factory_.GetWeakPtr(),
/*is_multi_page_scan=*/false)));
}
void ScanService::StartMultiPageScan(
const base::UnguessableToken& scanner_id,
scanning::mojom::ScanSettingsPtr settings,
mojo::PendingRemote<scanning::mojom::ScanJobObserver> observer,
StartMultiPageScanCallback callback) {
if (multi_page_controller_receiver_.is_bound()) {
LOG(ERROR) << "Unable to start multi-page scan, controller already bound.";
std::move(callback).Run(mojo::NullRemote());
return;
}
ClearScanState();
SetScanJobObserver(std::move(observer));
wake_lock_ = RequestWakeLock("Multi Page Scan in Progress");
const bool success = SendScanRequest(
scanner_id, std::move(settings), /*page_index_to_replace=*/std::nullopt,
base::BindOnce(&ScanService::OnMultiPageScanPageCompleted,
weak_ptr_factory_.GetWeakPtr()));
if (!success) {
std::move(callback).Run(mojo::NullRemote());
return;
}
mojo::PendingRemote<scanning::mojom::MultiPageScanController> pending_remote =
multi_page_controller_receiver_.BindNewPipeAndPassRemote();
// This allows a multi-page scan session to be cancelled by resetting the
// message pipe.
multi_page_controller_receiver_.set_disconnect_handler(
base::BindOnce(&ScanService::ResetMultiPageScanController,
weak_ptr_factory_.GetWeakPtr()));
std::move(callback).Run(std::move(pending_remote));
multi_page_start_time_ = base::TimeTicks::Now();
}
bool ScanService::SendScanRequest(
const base::UnguessableToken& scanner_id,
mojo_ipc::ScanSettingsPtr settings,
const std::optional<uint32_t> page_index_to_replace,
base::OnceCallback<void(lorgnette::ScanFailureMode failure_mode)>
completion_callback) {
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);
// Save the DPI information for the scan.
scan_dpi_ = settings->resolution_dpi;
timeout_callback_.Reset(base::BindOnce(&ScanService::OnScanCompleted));
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
// If this callback is called, `is_multi_page_scan` does not matter.
// Always setting it to false here makes it simpler.
FROM_HERE, base::BindOnce(timeout_callback_.callback(), this,
/*is_multi_page_scan=*/false,
lorgnette::SCAN_FAILURE_MODE_IO_ERROR),
kTimeout);
start_time_ = base::Time::Now();
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, page_index_to_replace),
std::move(completion_callback));
return true;
}
void ScanService::CancelScan() {
// In case the user cancels, we don't want to cancel again in the future.
timeout_callback_.Cancel();
lorgnette_scanner_manager_->CancelScan(base::BindOnce(
&ScanService::OnCancelCompleted, weak_ptr_factory_.GetWeakPtr()));
}
void ScanService::ScanNextPage(const base::UnguessableToken& scanner_id,
scanning::mojom::ScanSettingsPtr settings,
ScanNextPageCallback callback) {
std::move(callback).Run(SendScanRequest(
scanner_id, std::move(settings), /*page_index_to_replace=*/std::nullopt,
base::BindOnce(&ScanService::OnMultiPageScanPageCompleted,
weak_ptr_factory_.GetWeakPtr())));
}
void ScanService::RemovePage(uint32_t page_index) {
if (page_index >= scanned_images_.size()) {
multi_page_controller_receiver_.ReportBadMessage(
"Invalid page_index passed to ScanService::RemovePage()");
return;
}
if (scanned_images_.size() == 0) {
multi_page_controller_receiver_.ReportBadMessage(
"Invalid call to ScanService::RemovePage(), no scanned images "
"available to remove");
return;
}
base::UmaHistogramEnumeration(
"Scanning.MultiPageScan.ToolbarAction",
scanning::ScanMultiPageToolbarAction::kRemovePage);
if (scanned_images_.size() == 1) {
ClearScanState();
multi_page_controller_receiver_.reset();
return;
}
scanned_images_.erase(scanned_images_.begin() + page_index);
--num_pages_scanned_;
}
void ScanService::RescanPage(const base::UnguessableToken& scanner_id,
scanning::mojom::ScanSettingsPtr settings,
uint32_t page_index,
ScanNextPageCallback callback) {
if (scanned_images_.size() == 0) {
multi_page_controller_receiver_.ReportBadMessage(
"Invalid call to ScanService::RescanPage(), no scanned images "
"available to rescan");
return;
}
if (page_index >= scanned_images_.size()) {
multi_page_controller_receiver_.ReportBadMessage(
"Invalid page_index passed to ScanService::RescanPage()");
return;
}
std::move(callback).Run(
SendScanRequest(scanner_id, std::move(settings), page_index,
base::BindOnce(&ScanService::OnMultiPageScanPageCompleted,
weak_ptr_factory_.GetWeakPtr())));
base::UmaHistogramEnumeration(
"Scanning.MultiPageScan.ToolbarAction",
scanning::ScanMultiPageToolbarAction::kRescanPage);
}
void ScanService::CompleteMultiPageScan() {
OnScanCompleted(/*is_multi_page_scan=*/true,
lorgnette::SCAN_FAILURE_MODE_NO_FAILURE);
base::UmaHistogramCounts100("Scanning.MultiPageScan.NumPagesScanned",
num_pages_scanned_);
base::UmaHistogramLongTimes100(
"Scanning.MultiPageScan.SessionDuration",
base::TimeTicks::Now() - multi_page_start_time_);
multi_page_start_time_ = base::TimeTicks();
multi_page_controller_receiver_.reset();
}
void ScanService::BindInterface(
mojo::PendingReceiver<mojo_ipc::ScanService> pending_receiver) {
receiver_.reset();
receiver_.Bind(std::move(pending_receiver));
}
void ScanService::Shutdown() {
timeout_callback_.Cancel();
lorgnette_scanner_manager_ = nullptr;
receiver_.reset();
multi_page_controller_receiver_.reset();
weak_ptr_factory_.InvalidateWeakPtrs();
wake_lock_.reset();
}
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 std::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,
const std::optional<uint32_t> page_index_to_replace,
std::string scanned_image,
uint32_t page_number) {
timeout_callback_.Cancel();
// 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);
uint32_t new_page_index;
if (file_type == mojo_ipc::FileType::kPdf) {
new_page_index = page_index_to_replace.has_value()
? page_index_to_replace.value()
: scanned_images_.size();
} else {
// Non-PDF scans do not save images in |scanned_images_| so the next index
// is based off |page_number|.
DCHECK(!page_index_to_replace.has_value());
new_page_index = page_number - 1;
}
scan_job_observer_->OnPageComplete(
std::vector<uint8_t>(scanned_image.begin(), scanned_image.end()),
new_page_index);
// Only increment the |num_pages_scanned_| tracker if appending, not
// replacing, a page.
if (!page_index_to_replace.has_value()) {
++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) {
if (!page_index_to_replace.has_value()) {
scanned_images_.push_back(std::move(scanned_image));
} else {
DCHECK(page_index_to_replace.value() >= 0 &&
page_index_to_replace.value() < scanned_images_.size());
scanned_images_[page_index_to_replace.value()] = 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(1u, page_number);
scanned_file_paths_.push_back(scan_to_path.Append(CreateFilename(
start_time_, /*not used*/ 0, mojo_ipc::FileType::kPdf)));
}
return;
}
task_runner_->PostTaskAndReplyWithResult(
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(bool is_multi_page_scan,
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());
timeout_callback_.Cancel();
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&SaveAsPdf, scanned_images_, scanned_file_paths_.back(),
rotate_alternate_pages_, is_multi_page_scan, scan_dpi_),
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.
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](lorgnette::ScanFailureMode failure_mode) { return failure_mode; },
failure_mode),
base::BindOnce(&ScanService::OnAllPagesSaved,
weak_ptr_factory_.GetWeakPtr()));
wake_lock_.reset();
}
void ScanService::OnMultiPageScanPageCompleted(
lorgnette::ScanFailureMode failure_mode) {
if (failure_mode ==
lorgnette::ScanFailureMode::SCAN_FAILURE_MODE_NO_FAILURE) {
base::UmaHistogramEnumeration("Scanning.MultiPageScan.PageScanResult",
scanning::ScanJobFailureReason::kSuccess);
// Reset the timeout for each page in a multi page scan so timeout isn't
// triggered for a long scan.
timeout_callback_.Cancel();
return;
}
scan_job_observer_->OnMultiPageScanFail(failure_mode);
base::UmaHistogramEnumeration("Scanning.MultiPageScan.PageScanResult",
GetScanJobFailureReason(failure_mode));
}
void ScanService::OnCancelCompleted(bool success) {
if (success)
ClearScanState();
scan_job_observer_->OnCancelComplete(success);
wake_lock_.reset();
}
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) {
std::optional<scanning::ScanJobFailureReason> failure_reason = std::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(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->AddItemOfType(HoldingSpaceItem::Type::kScan,
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;
scan_dpi_ = std::nullopt;
scanned_file_paths_.clear();
scanned_images_.clear();
num_pages_scanned_ = 0;
wake_lock_.reset();
}
void ScanService::SetScanJobObserver(
mojo::PendingRemote<mojo_ipc::ScanJobObserver> observer) {
scan_job_observer_.reset();
scan_job_observer_.Bind(std::move(observer));
scan_job_observer_.set_disconnect_handler(
base::BindOnce(&ScanService::CancelScan, weak_ptr_factory_.GetWeakPtr()));
}
void ScanService::ResetMultiPageScanController() {
multi_page_controller_receiver_.reset();
ClearScanState();
}
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;
}
std::vector<std::string> ScanService::GetScannedImagesForTesting() const {
return scanned_images_;
}
} // namespace ash