chromium/chrome/browser/ash/printing/automatic_usb_printer_configurer.cc

// Copyright 2019 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/printing/automatic_usb_printer_configurer.h"

#include <string>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/ash/printing/printer_installation_manager.h"
#include "chrome/browser/ash/printing/usb_printer_notification_controller.h"
#include "chromeos/printing/ppd_provider.h"
#include "components/device_event_log/device_event_log.h"

namespace ash {

AutomaticUsbPrinterConfigurer::AutomaticUsbPrinterConfigurer(
    PrinterInstallationManager* installation_manager,
    UsbPrinterNotificationController* notification_controller,
    chromeos::PpdProvider* ppd_provider,
    base::RepeatingCallback<void(std::string)> refresh_callback)
    : installation_manager_(installation_manager),
      notification_controller_(notification_controller),
      ppd_provider_(ppd_provider),
      refresh_callback_(refresh_callback) {
  DCHECK(installation_manager);
  DCHECK(notification_controller);
  DCHECK(ppd_provider);
}

AutomaticUsbPrinterConfigurer::~AutomaticUsbPrinterConfigurer() = default;

void AutomaticUsbPrinterConfigurer::UpdateListOfConnectedPrinters(
    std::vector<PrinterDetector::DetectedPrinter> new_list) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

  // Calculate lists of added, existing and removed printers.
  base::flat_set<std::string> added;
  base::flat_set<std::string> existing;
  base::flat_set<std::string> removed;
  for (PrinterDetector::DetectedPrinter& detected : new_list) {
    const std::string& id = detected.printer.id();
    if (base::Contains(connected_printers_, id)) {
      existing.insert(id);
    } else {
      added.insert(id);
      connected_printers_[id] = std::move(detected);
    }
  }
  for (const auto& [id, detected] : connected_printers_) {
    if (!base::Contains(added, id) && !base::Contains(existing, id)) {
      removed.insert(id);
    }
  }

  // Process removed printers.
  for (const std::string& id : removed) {
    notification_controller_->RemoveNotification(id);
    installation_manager_->UninstallPrinter(id);
    connected_printers_.erase(id);
    configured_printers_.erase(id);
    unconfigured_printers_.erase(id);
    ppd_references_.erase(id);
  }

  // Process added printers.
  for (const std::string& id : added) {
    ConfigurePrinter(id);
  }
}

void AutomaticUsbPrinterConfigurer::ConfigurePrinter(
    const std::string& printer_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

  PrinterDetector::DetectedPrinter& detected = connected_printers_[printer_id];

  if (detected.printer.RequiresDriverlessUsb()) {
    // This model should attempt autoconfiguration with IPP-USB instead of
    // looking up a PPD for the USB printer class.
    // We copy the printer's object to adjust its configuration before setup.
    // The new printer's configuration is saved to `connected_printers_` only if
    // the setup succeeds.
    chromeos::Printer printer = detected.printer;
    printer.SetUri(chromeos::Uri(base::StringPrintf(
        "ippusb://%04x_%04x/ipp/print", detected.ppd_search_data.usb_vendor_id,
        detected.ppd_search_data.usb_product_id)));
    printer.mutable_ppd_reference()->autoconf = true;
    installation_manager_->SetUpPrinter(
        printer, /*is_automatic_installation=*/true,
        base::BindOnce(&AutomaticUsbPrinterConfigurer::OnSetupComplete,
                       weak_factory_.GetWeakPtr(), printer));
    return;
  }

  // We can start PPD resolution only if there is no other pending resolution
  // for the same `printer_id`. This may happen when a user connects and
  // disconnects USB printer several times in a short period of time.
  if (pending_ppd_resolutions_.insert(printer_id).second) {
    // Insertion took place.
    ppd_provider_->ResolvePpdReference(
        detected.ppd_search_data,
        base::BindOnce(
            &AutomaticUsbPrinterConfigurer::OnResolvePpdReferenceDone,
            weak_factory_.GetWeakPtr(), printer_id));
  }
}

// Callback invoked on completion of PpdProvider::ResolvePpdReference.
void AutomaticUsbPrinterConfigurer::OnResolvePpdReferenceDone(
    const std::string& printer_id,
    chromeos::PpdProvider::CallbackResultCode code,
    const chromeos::Printer::PpdReference& ref,
    const std::string& usb_manufacturer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

  pending_ppd_resolutions_.erase(printer_id);
  auto it = connected_printers_.find(printer_id);
  if (it == connected_printers_.end()) {
    return;
  }

  PrinterDetector::DetectedPrinter& detected = it->second;

  if (code != chromeos::PpdProvider::SUCCESS) {
    PRINTER_LOG(EVENT) << "Failed to resolve PPD reference for " << printer_id;
    if (!detected.printer.supports_ippusb()) {
      // Detected printer does not supports ipp-over-usb, so we cannot set it
      // up automatically. We have to leave it as unconfigured.
      if (!usb_manufacturer.empty()) {
        detected.printer.set_usb_printer_manufacturer(usb_manufacturer);
      }
      FinalizeConfiguration(detected.printer, /*success=*/false);
      return;
    }
  }

  // We copy the printer's object to adjust its configuration before setup. The
  // new printer's configuration is saved to `connected_printers_` only if the
  // setup succeeds.
  chromeos::Printer printer = detected.printer;
  // If the printer supports ipp-over-usb and has a PPD, it will be affected by
  // the PPD -> IPP-over-USB migration. We need to mark it to gather extra info
  // in dedicated histograms.
  if (printer.supports_ippusb() && code == chromeos::PpdProvider::SUCCESS) {
    printer.SetAffectedByIppUsbMigration(true);
  }

  // Experimental path (b/184293121).
  const bool force_ipp =
      detected.printer.supports_ippusb() &&
      base::FeatureList::IsEnabled(features::kIppFirstSetupForUsbPrinters);

  if (code == chromeos::PpdProvider::SUCCESS && !force_ipp) {
    *printer.mutable_ppd_reference() = ref;
  } else {
    // Detected printer supports ipp-over-usb. We can try to set it up
    // automatically (by IPP Everywhere). We have to switch to the ippusb
    // scheme.
    printer.SetUri(chromeos::Uri(base::StringPrintf(
        "ippusb://%04x_%04x/ipp/print", detected.ppd_search_data.usb_vendor_id,
        detected.ppd_search_data.usb_product_id)));
    printer.mutable_ppd_reference()->autoconf = true;
    // If we have PpdReference, we save it for later.
    if (code == chromeos::PpdProvider::SUCCESS) {
      ppd_references_[printer_id] = ref;
    }
  }

  installation_manager_->SetUpPrinter(
      printer, /*is_automatic_installation=*/true,
      base::BindOnce(&AutomaticUsbPrinterConfigurer::OnSetupComplete,
                     weak_factory_.GetWeakPtr(), printer));
}

void AutomaticUsbPrinterConfigurer::OnSetupComplete(
    const chromeos::Printer& printer,
    PrinterSetupResult result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

  auto it = connected_printers_.find(printer.id());
  if (it == connected_printers_.end()) {
    return;
  }

  if (printer.AffectedByIppUsbMigration()) {
    base::UmaHistogramEnumeration(
        "Printing.CUPS.AutomaticSetupResultOfUsbPrinterSupportingIppAndPpd",
        result);
  }

  auto it_ref = ppd_references_.find(printer.id());
  if (it_ref != ppd_references_.end()) {
    // We have a PPD for this printer. We can try to use it if IPP Everywhere
    // setup failed.
    if (result == PrinterSetupResult::kPrinterIsNotAutoconfigurable) {
      // Repeat the setup procedure with the given PPD file.
      chromeos::Printer ppd_printer = it->second.printer;
      *ppd_printer.mutable_ppd_reference() = std::move(it_ref->second);
      ppd_references_.erase(it_ref);
      installation_manager_->SetUpPrinter(
          ppd_printer, /*is_automatic_installation=*/true,
          base::BindOnce(&AutomaticUsbPrinterConfigurer::OnSetupComplete,
                         weak_factory_.GetWeakPtr(), ppd_printer));
      return;
    }
    // Nevermind. Just remove it.
    ppd_references_.erase(it_ref);
  }

  const bool success = (result == PrinterSetupResult::kSuccess);
  if (success) {
    it->second.printer = printer;
  }
  FinalizeConfiguration(it->second.printer, success);
}

void AutomaticUsbPrinterConfigurer::FinalizeConfiguration(
    const chromeos::Printer& printer,
    bool success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
  DCHECK(!configured_printers_.contains(printer.id()));
  DCHECK(!unconfigured_printers_.contains(printer.id()));

  if (success) {
    // The printer is ready to use
    PrinterConfigurer::RecordUsbPrinterSetupSource(
        UsbPrinterSetupSource::kAutoconfigured);
    PRINTER_LOG(EVENT) << "Auto USB Printer setup successful for "
                       << printer.id();
    notification_controller_->ShowEphemeralNotification(printer);
    configured_printers_.insert(printer.id());
  } else {
    // The printer cannot be configured automatically
    PRINTER_LOG(EVENT) << "Unable to autoconfigure usb printer "
                       << printer.id();
    notification_controller_->ShowConfigurationNotification(printer);
    unconfigured_printers_.insert(printer.id());
  }
  refresh_callback_.Run(printer.id());
}

}  // namespace ash