910e62b5创建于 1月15日历史提交
// Copyright 2025 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/simple_scan_runner.h"

#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/ash/scanning/lorgnette_scanner_manager.h"
#include "chrome/browser/ash/scanning/lorgnette_scanner_manager_factory.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 {

// Error messages that can be included in a response when scanning fails.
constexpr char kNoScannersAvailableError[] = "No scanners available";
constexpr char kScanImageError[] = "Failed to scan image";
constexpr char kUnsupportedMimeTypesError[] = "Unsupported MIME types";
constexpr char kVirtualPrinterUnavailableError[] =
    "Virtual USB printer unavailable";

// Special MIME type that triggers use of virtual-usb-printer for scanning.
constexpr char kTestingMimeType[] = "testing";

// The name of the virtual USB printer used for testing.
constexpr char kVirtualUSBPrinter[] = "DavieV Virtual USB Printer (USB)";

// The PNG MIME type.
constexpr char kScannerImageMimeTypePng[] = "image/png";

// The PNG image data URL prefix of a scanned image.
constexpr char kPngImageDataUrlPrefix[] = "data:image/png;base64,";

// The delay between reads from the scanner when data isn't expected to be ready
// immediately.
constexpr base::TimeDelta kSlowReadInterval = base::Milliseconds(500);

// The delay between reads from the scanner when data might be ready.
constexpr base::TimeDelta kReadInterval = base::Milliseconds(100);

// The connection type name for Mopria eSCL scanners.
constexpr char kMopriaProtocolName[] = "Mopria";

}  // namespace

SimpleScanRunner::SimpleScanRunner(content::BrowserContext* browser_context,
                                   scoped_refptr<const Extension> extension,
                                   crosapi::mojom::DocumentScan* document_scan)
    : browser_context_(browser_context),
      extension_(std::move(extension)),
      document_scan_(document_scan) {
  CHECK(browser_context_);
  CHECK(extension_);
}

SimpleScanRunner::~SimpleScanRunner() = default;

void SimpleScanRunner::Start(std::vector<std::string> mime_types,
                             SimpleScanCallback callback) {
  CHECK(!callback_) << "scan call already in progress";
  callback_ = std::move(callback);
  mime_types_ = std::move(mime_types);

  // Clear any leftover state from a previous scan.
  scanner_ids_.clear();
  scanner_handle_ = "";
  job_handle_ = "";
  scan_data_.clear();
  scan_result_ = crosapi::mojom::ScanFailureMode::kUnknown;

  bool should_use_virtual_usb_printer = false;
  if (base::Contains(mime_types_, kTestingMimeType)) {
    should_use_virtual_usb_printer = true;
  } else if (!base::Contains(mime_types_, kScannerImageMimeTypePng)) {
    std::move(callback_).Run(std::nullopt, kUnsupportedMimeTypesError);
    return;
  }

  ash::LorgnetteScannerManagerFactory::GetForBrowserContext(browser_context_)
      ->GetScannerInfoList(
          extension_id(),
          ash::LorgnetteScannerManager::LocalScannerFilter::
              kIncludeNetworkScanners,
          ash::LorgnetteScannerManager::SecureScannerFilter::
              kIncludeUnsecureScanners,
          base::BindOnce(&SimpleScanRunner::OnSimpleScanListReceived,
                         weak_ptr_factory_.GetWeakPtr(),
                         should_use_virtual_usb_printer));
}

const ExtensionId& SimpleScanRunner::extension_id() const {
  return extension_->id();
}

void SimpleScanRunner::OnSimpleScanListReceived(
    bool force_virtual_usb_printer,
    const std::optional<lorgnette::ListScannersResponse>& response) {
  if (!response.has_value() || response->scanners().empty()) {
    std::move(callback_).Run(std::nullopt, kNoScannersAvailableError);
    return;
  }

  std::vector<const lorgnette::ScannerInfo*> scanners;
  scanners.reserve(response->scanners().size());
  for (const auto& scanner : response->scanners()) {
    scanners.push_back(&scanner);
  }

  // A scanner source needs to be chosen.  Since the choice is unspecified, sort
  // the list with these heuristics and take the first one that can be
  // successfully opened:
  //   1.  If force_virtual_usb_printer is true, always pick the virtual USB
  //       printer.
  //   2.  USB scanners come first, since they are both local and secure.
  //   3.  Secure network scanners come next.
  //   4.  Insecure network scanners come last.
  // Within each grouping, prefer Mopria eSCL to legacy protocols, since the
  // backend is known to work consistently.
  std::ranges::stable_sort(
      scanners, std::less<>{}, [](const lorgnette::ScannerInfo* info) {
        return std::tuple(
            // Virtual USB printer always comes first.
            info->display_name() != kVirtualUSBPrinter,

            // USB devices come first.
            info->connection_type() !=
                lorgnette::ConnectionType::CONNECTION_USB,

            // Secure devices come before insecure.
            !info->secure(),

            // Mopria/eSCL devices come before legacy devices.
            info->protocol_type() != kMopriaProtocolName,

            // Sort by display name if all else is equal.
            info->display_name());
      });

  if (force_virtual_usb_printer &&
      scanners[0]->display_name() != kVirtualUSBPrinter) {
    std::move(callback_).Run(std::nullopt, kVirtualPrinterUnavailableError);
    return;
  }

  // Store the list of IDs in reverse so it can be processed more efficiently in
  // the callbacks.  The rest of the ScannerInfo fields aren't needed.
  scanner_ids_.reserve(scanners.size());
  for (const lorgnette::ScannerInfo* info : scanners) {
    if (force_virtual_usb_printer &&
        info->display_name() != kVirtualUSBPrinter) {
      continue;
    }
    scanner_ids_.push_back(std::move(info->name()));
  }
  std::ranges::reverse(scanner_ids_);

  OpenFirstScanner();
}

void SimpleScanRunner::OpenFirstScanner() {
  if (scanner_ids_.empty()) {
    std::move(callback_).Run(std::nullopt, kNoScannersAvailableError);
    return;
  }

  std::string scanner_id = std::move(scanner_ids_.back());
  scanner_ids_.pop_back();
  document_scan_->OpenScanner(
      extension_id(), std::move(scanner_id),
      base::BindOnce(&SimpleScanRunner::OnOpenScannerResponse,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SimpleScanRunner::OnOpenScannerResponse(
    crosapi::mojom::OpenScannerResponsePtr response) {
  if (response->result != crosapi::mojom::ScannerOperationResult::kSuccess ||
      !response->scanner_handle.has_value()) {
    OpenFirstScanner();
    return;
  }
  scanner_handle_ = std::move(response->scanner_handle.value());

  auto options = crosapi::mojom::StartScanOptions::New();
  options->format = kScannerImageMimeTypePng;

  document_scan_->StartPreparedScan(
      scanner_handle_, std::move(options),
      base::BindOnce(&SimpleScanRunner::OnStartPreparedScanResponse,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SimpleScanRunner::OnStartPreparedScanResponse(
    crosapi::mojom::StartPreparedScanResponsePtr response) {
  if (response->result != crosapi::mojom::ScannerOperationResult::kSuccess ||
      !response->job_handle.has_value()) {
    // Closing the scanner will also return the response to the caller.
    document_scan_->CloseScanner(
        scanner_handle_,
        base::BindOnce(&SimpleScanRunner::OnCloseScannerResponse,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }

  // Scanners normally don't produce bytes right away, so start the read loop
  // after a delay.
  job_handle_ = std::move(response->job_handle.value());
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&SimpleScanRunner::ReadScanData,
                     weak_ptr_factory_.GetWeakPtr()),
      kSlowReadInterval);
}

void SimpleScanRunner::ReadScanData() {
  document_scan_->ReadScanData(
      job_handle_, base::BindOnce(&SimpleScanRunner::OnReadScanDataResponse,
                                  weak_ptr_factory_.GetWeakPtr()));
}

void SimpleScanRunner::OnReadScanDataResponse(
    crosapi::mojom::ReadScanDataResponsePtr response) {
  // Success means to keep going.  If data was ready, append it to what we got
  // so far.
  if (response->result == crosapi::mojom::ScannerOperationResult::kSuccess) {
    if (response->data.has_value() && response->data->size() > 0) {
      scan_data_.insert(scan_data_.end(), response->data->begin(),
                        response->data->end());
    }

    // Once the first byte after the image headers is received, poll the scanner
    // more quickly because data usually streams consistently.
    base::TimeDelta delay =
        (scan_data_.size() > 100) ? kReadInterval : kSlowReadInterval;
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&SimpleScanRunner::ReadScanData,
                       weak_ptr_factory_.GetWeakPtr()),
        delay);
    return;
  }

  // EOF means no more data is available.  There might be a final data chunk.
  if (response->result == crosapi::mojom::ScannerOperationResult::kEndOfData) {
    if (response->data.has_value() && response->data->size() > 0) {
      scan_data_.insert(scan_data_.end(), response->data->begin(),
                        response->data->end());
    }

    scan_result_ = crosapi::mojom::ScanFailureMode::kNoFailure;
  }

  document_scan_->CloseScanner(
      scanner_handle_, base::BindOnce(&SimpleScanRunner::OnCloseScannerResponse,
                                      weak_ptr_factory_.GetWeakPtr()));
}

void SimpleScanRunner::OnCloseScannerResponse(
    crosapi::mojom::CloseScannerResponsePtr) {
  // Intentionally ignore the response.  The result to return to the caller has
  // already been determined at the end of the read loop.
  OnSimpleScanCompleted(scan_result_);
}

void SimpleScanRunner::OnSimpleScanCompleted(
    crosapi::mojom::ScanFailureMode failure_mode) {
  if (!scan_data_.size() ||
      failure_mode != crosapi::mojom::ScanFailureMode::kNoFailure) {
    std::move(callback_).Run(std::nullopt, kScanImageError);
    return;
  }

  std::string image_base64 = base::Base64Encode(scan_data_);
  api::document_scan::ScanResults scan_results;
  scan_results.data_urls.push_back(kPngImageDataUrlPrefix +
                                   std::move(image_base64));
  scan_results.mime_type = kScannerImageMimeTypePng;

  std::move(callback_).Run(std::move(scan_results), std::nullopt);
}

}  // namespace extensions