chromium/chrome/browser/extensions/api/document_scan/start_scan_runner.cc

// 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