| // 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/native_window_tracker/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_ = ui::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 |