// Copyright 2023 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/extensions/api/document_scan/start_scan_runner.h"
#include "base/containers/contains.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extensions_dialogs.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/document_scan.mojom.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/image_loader.h"
#include "extensions/common/extension.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/native_window_tracker.h"
namespace extensions {
namespace {
// Icon size for confirmation dialogs.
constexpr int kIconSize = 64;
// There is no easy way to interact with UI dialogs that are generated by Chrome
// itself, so we need to have a way to bypass this for testing.
std::optional<bool> g_start_scan_confirmation_result = std::nullopt;
bool CanSkipConfirmation(content::BrowserContext* browser_context,
const ExtensionId& extension_id) {
const base::Value::List& list =
Profile::FromBrowserContext(browser_context)
->GetPrefs()
->GetList(prefs::kDocumentScanAPITrustedExtensions);
return base::Contains(list, base::Value(extension_id));
// TODO(b/312740272): Add a way for the user to make their consent permanent.
// Note that this needs to be per device.
}
} // namespace
StartScanRunner::StartScanRunner(gfx::NativeWindow native_window,
content::BrowserContext* browser_context,
scoped_refptr<const Extension> extension,
crosapi::mojom::DocumentScan* document_scan)
: native_window_(native_window),
browser_context_(browser_context),
extension_(std::move(extension)),
document_scan_(document_scan),
approved_(false) {
CHECK(extension_);
if (native_window_) {
native_window_tracker_ = views::NativeWindowTracker::Create(native_window_);
}
}
StartScanRunner::~StartScanRunner() = default;
// static
base::AutoReset<std::optional<bool>>
StartScanRunner::SetStartScanConfirmationResultForTesting(bool val) {
return base::AutoReset<std::optional<bool>>(&g_start_scan_confirmation_result,
val);
}
void StartScanRunner::Start(bool is_approved,
const std::string& scanner_name,
const std::string& scanner_handle,
crosapi::mojom::StartScanOptionsPtr options,
StartScanCallback callback) {
CHECK(!callback_) << "start scan call already in progress";
callback_ = std::move(callback);
options_ = std::move(options);
scanner_handle_ = std::move(scanner_handle);
// TODO(b/312740272): Skip confirmation prompt if previous consent was within
// the recent past (specific timeout TBD). Note that confirmation needs to be
// per device.
if (is_approved || CanSkipConfirmation(browser_context_, extension_->id())) {
SendStartScanRequest();
return;
}
// If a test has set the confirmation result, go directly to the end handler
// instead of displaying the dialog.
if (g_start_scan_confirmation_result) {
OnConfirmationDialogClosed(g_start_scan_confirmation_result.value());
return;
}
ImageLoader::Get(browser_context_)
->LoadImageAtEveryScaleFactorAsync(
extension_.get(), gfx::Size(kIconSize, kIconSize),
base::BindOnce(&StartScanRunner::ShowStartScanDialog,
weak_ptr_factory_.GetWeakPtr(), scanner_name));
}
const ExtensionId& StartScanRunner::extension_id() const {
return extension_->id();
}
void StartScanRunner::ShowStartScanDialog(const std::string& scanner_name,
const gfx::Image& icon) {
// If the browser window was closed during API request handling, treat it the
// same as if the user denied the request.
if (native_window_tracker_ &&
native_window_tracker_->WasNativeWindowDestroyed()) {
OnConfirmationDialogClosed(false);
return;
}
ShowDocumentScannerStartScanConfirmationDialog(
native_window_, extension_->id(), base::UTF8ToUTF16(extension_->name()),
base::UTF8ToUTF16(scanner_name), icon.AsImageSkia(),
base::BindOnce(&StartScanRunner::OnConfirmationDialogClosed,
weak_ptr_factory_.GetWeakPtr()));
}
void StartScanRunner::OnConfirmationDialogClosed(bool approved) {
if (approved) {
SendStartScanRequest();
return;
}
auto response = crosapi::mojom::StartPreparedScanResponse::New();
response->result = crosapi::mojom::ScannerOperationResult::kAccessDenied;
response->scanner_handle = scanner_handle_;
std::move(callback_).Run(std::move(response));
}
void StartScanRunner::SendStartScanRequest() {
approved_ = true;
document_scan_->StartPreparedScan(
scanner_handle_, std::move(options_),
base::BindOnce(&StartScanRunner::OnStartScanResponse,
weak_ptr_factory_.GetWeakPtr()));
// TODO(b/312757530): Clean up the pending call if the DocumentScan service
// goes away without running our callback.
}
void StartScanRunner::OnStartScanResponse(
crosapi::mojom::StartPreparedScanResponsePtr response) {
std::move(callback_).Run(std::move(response));
}
} // namespace extensions