chromium/chromeos/ash/components/dbus/lorgnette_manager/lorgnette_manager_client.cc

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/dbus/lorgnette_manager/lorgnette_manager_client.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chromeos/ash/components/dbus/lorgnette/lorgnette_service.pb.h"
#include "chromeos/ash/components/dbus/lorgnette_manager/fake_lorgnette_manager_client.h"
#include "chromeos/dbus/common/pipe_reader.h"
#include "components/device_event_log/device_event_log.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {
namespace {

LorgnetteManagerClient* g_instance = nullptr;

constexpr base::TimeDelta kDiscoveryMonitorInterval = base::Seconds(1);
constexpr base::TimeDelta kDiscoveryMaxInactivity = base::Seconds(20);
constexpr base::TimeDelta kSlowOperationTimeout = base::Seconds(60);

// The LorgnetteManagerClient implementation used in production.
class LorgnetteManagerClientImpl : public LorgnetteManagerClient {
 public:
  LorgnetteManagerClientImpl() = default;
  LorgnetteManagerClientImpl(const LorgnetteManagerClientImpl&) = delete;
  LorgnetteManagerClientImpl& operator=(const LorgnetteManagerClientImpl&) =
      delete;
  ~LorgnetteManagerClientImpl() override = default;

  void ListScanners(
      const std::string& client_id,
      bool local_only,
      bool preferred_only,
      chromeos::DBusMethodCallback<lorgnette::ListScannersResponse> callback)
      override {
    // The client ID is required for asynchronous discovery.  If none is
    // provided, exit early with an error result.
    if (client_id.empty()) {
      lorgnette::ListScannersResponse response;
      response.set_result(lorgnette::OPERATION_RESULT_INVALID);
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::move(response)));
      return;
    }

    lorgnette::StartScannerDiscoveryRequest request;
    request.set_client_id(client_id);
    request.set_preferred_only(preferred_only);
    request.set_local_only(local_only);

    StartScannerDiscovery(
        std::move(request),
        base::BindRepeating(
            &LorgnetteManagerClientImpl::ListScannersDiscoveryScannersUpdated,
            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(
            &LorgnetteManagerClientImpl::OnListScannersDiscoverySession,
            weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void GetScannerCapabilities(
      const std::string& device_name,
      chromeos::DBusMethodCallback<lorgnette::ScannerCapabilities> callback)
      override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kGetScannerCapabilitiesMethod);
    dbus::MessageWriter writer(&method_call);
    writer.AppendString(device_name);
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(
            &LorgnetteManagerClientImpl::OnScannerCapabilitiesResponse,
            weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void OpenScanner(const lorgnette::OpenScannerRequest& request,
                   chromeos::DBusMethodCallback<lorgnette::OpenScannerResponse>
                       callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kOpenScannerMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode OpenScannerRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnOpenScannerResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void CloseScanner(
      const lorgnette::CloseScannerRequest& request,
      chromeos::DBusMethodCallback<lorgnette::CloseScannerResponse> callback)
      override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kCloseScannerMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode CloseScannerRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnCloseScannerResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void SetOptions(const lorgnette::SetOptionsRequest& request,
                  chromeos::DBusMethodCallback<lorgnette::SetOptionsResponse>
                      callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kSetOptionsMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode SetOptionsRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnSetOptionsResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void GetCurrentConfig(
      const lorgnette::GetCurrentConfigRequest& request,
      chromeos::DBusMethodCallback<lorgnette::GetCurrentConfigResponse>
          callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kGetCurrentConfigMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode GetCurrentConfigRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnGetCurrentConfigResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void StartPreparedScan(
      const lorgnette::StartPreparedScanRequest& request,
      chromeos::DBusMethodCallback<lorgnette::StartPreparedScanResponse>
          callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kStartPreparedScanMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode StartPreparedScanRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, kSlowOperationTimeout.InMilliseconds(),
        base::BindOnce(&LorgnetteManagerClientImpl::OnStartPreparedScanResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void StartScan(
      const std::string& device_name,
      const lorgnette::ScanSettings& settings,
      base::OnceCallback<void(lorgnette::ScanFailureMode)> completion_callback,
      base::RepeatingCallback<void(std::string, uint32_t)> page_callback,
      base::RepeatingCallback<void(uint32_t, uint32_t)> progress_callback)
      override {
    lorgnette::StartScanRequest request;
    request.set_device_name(device_name);
    *request.mutable_settings() = settings;

    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kStartScanMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode StartScanRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(completion_callback),
                                    lorgnette::SCAN_FAILURE_MODE_UNKNOWN));
      return;
    }

    ScanJobState state;
    state.completion_callback = std::move(completion_callback);
    state.progress_callback = std::move(progress_callback);
    state.page_callback = std::move(page_callback);

    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnStartScanResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(state)));
  }

  void ReadScanData(
      const lorgnette::ReadScanDataRequest& request,
      chromeos::DBusMethodCallback<lorgnette::ReadScanDataResponse> callback)
      override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kReadScanDataMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode ReadScanDataRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, kSlowOperationTimeout.InMilliseconds(),
        base::BindOnce(&LorgnetteManagerClientImpl::OnReadScanDataResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void CancelScan(chromeos::VoidDBusMethodCallback cancel_callback) override {
    // Post the task to the proper sequence (since it requires access to
    // scan_job_state_).
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&LorgnetteManagerClientImpl::DoScanCancel,
                                  weak_ptr_factory_.GetWeakPtr(),
                                  std::move(cancel_callback)));
  }

  void CancelScan(const lorgnette::CancelScanRequest& request,
                  chromeos::DBusMethodCallback<lorgnette::CancelScanResponse>
                      callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kCancelScanMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode CancelScanRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnCancelScanJobResponse,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void StartScannerDiscovery(
      const lorgnette::StartScannerDiscoveryRequest& request,
      base::RepeatingCallback<void(lorgnette::ScannerListChangedSignal)>
          signal_callback,
      chromeos::DBusMethodCallback<lorgnette::StartScannerDiscoveryResponse>
          response_callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kStartScannerDiscoveryMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode StartScannerDiscoveryRequest protobuf";

      lorgnette::StartScannerDiscoveryResponse response;
      response.set_started(false);

      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(response_callback), std::move(response)));
      return;
    }

    PRINTER_LOG(USER) << "Starting scanner discovery for client "
                      << request.client_id()
                      << ", local_only=" << request.local_only()
                      << ", preferred_only=" << request.preferred_only();

    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(
            &LorgnetteManagerClientImpl::OnStartScannerDiscoveryResponse,
            weak_ptr_factory_.GetWeakPtr(), std::move(request),
            std::move(signal_callback), std::move(response_callback)));
  }

  void StopScannerDiscovery(
      const lorgnette::StopScannerDiscoveryRequest& request,
      chromeos::DBusMethodCallback<lorgnette::StopScannerDiscoveryResponse>
          callback) override {
    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kStopScannerDiscoveryMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode StopScannerDiscoveryRequest protobuf";

      lorgnette::StopScannerDiscoveryResponse response;
      response.set_stopped(false);

      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::move(response)));
      return;
    }

    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(
            &LorgnetteManagerClientImpl::OnStopScannerDiscoveryResponse,
            weak_ptr_factory_.GetWeakPtr(), request.session_id(),
            std::move(callback)));
  }

  void Init(dbus::Bus* bus) override {
    lorgnette_daemon_proxy_ =
        bus->GetObjectProxy(lorgnette::kManagerServiceName,
                            dbus::ObjectPath(lorgnette::kManagerServicePath));
    lorgnette_daemon_proxy_->ConnectToSignal(
        lorgnette::kManagerServiceInterface,
        lorgnette::kScanStatusChangedSignal,
        base::BindRepeating(
            &LorgnetteManagerClientImpl::ScanStatusChangedReceived,
            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&LorgnetteManagerClientImpl::LorgnetteSignalConnected,
                       weak_ptr_factory_.GetWeakPtr()));
    lorgnette_daemon_proxy_->ConnectToSignal(
        lorgnette::kManagerServiceInterface,
        lorgnette::kScannerListChangedSignal,
        base::BindRepeating(
            &LorgnetteManagerClientImpl::ScannerListChangedReceived,
            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&LorgnetteManagerClientImpl::LorgnetteSignalConnected,
                       weak_ptr_factory_.GetWeakPtr()));
  }

 private:
  // Reads scan data on a blocking sequence.
  class ScanDataReader {
   public:
    // In case of success, std::string holds the read data. Otherwise,
    // nullopt.
    using CompletionCallback =
        base::OnceCallback<void(std::optional<std::string> data)>;

    ScanDataReader() = default;
    ScanDataReader(const ScanDataReader&) = delete;
    ScanDataReader& operator=(const ScanDataReader&) = delete;

    // Creates a pipe to read the scan data from the D-Bus service.
    // Returns a write-side FD.
    base::ScopedFD Start() {
      DCHECK(!pipe_reader_.get());
      DCHECK(!data_.has_value());
      pipe_reader_ = std::make_unique<chromeos::PipeReader>(
          base::ThreadPool::CreateTaskRunner(
              {base::MayBlock(),
               base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}));

      return pipe_reader_->StartIO(base::BindOnce(
          &ScanDataReader::OnDataRead, weak_ptr_factory_.GetWeakPtr()));
    }

    // Waits for the data read completion. If it is already done, |callback|
    // will be called synchronously.
    void Wait(CompletionCallback callback) {
      DCHECK(callback_.is_null());
      callback_ = std::move(callback);
      MaybeCompleted();
    }

   private:
    // Called when a |pipe_reader_| completes reading scan data to a string.
    void OnDataRead(std::optional<std::string> data) {
      DCHECK(!data_read_);
      data_read_ = true;
      data_ = std::move(data);
      pipe_reader_.reset();
      MaybeCompleted();
    }

    void MaybeCompleted() {
      // If data reading is not yet completed, or D-Bus call does not yet
      // return, wait for the other.
      if (!data_read_ || callback_.is_null())
        return;

      std::move(callback_).Run(std::move(data_));
    }

    std::unique_ptr<chromeos::PipeReader> pipe_reader_;

    // Set to true on data read completion.
    bool data_read_ = false;

    // Available only when |data_read_| is true.
    std::optional<std::string> data_;

    CompletionCallback callback_;

    base::WeakPtrFactory<ScanDataReader> weak_ptr_factory_{this};
  };

  // The state tracked for an in-progress scan job.
  // Contains callbacks used to report job progress, completion, failure, or
  // cancellation, as well as a ScanDataReader which is responsible for reading
  // from the pipe of data into a string.
  struct ScanJobState {
    base::OnceCallback<void(lorgnette::ScanFailureMode)> completion_callback;
    base::RepeatingCallback<void(uint32_t, uint32_t)> progress_callback;
    base::RepeatingCallback<void(std::string, uint32_t)> page_callback;
    chromeos::VoidDBusMethodCallback cancel_callback;
    std::unique_ptr<ScanDataReader> scan_data_reader;
  };

  struct DiscoverySessionState {
    std::optional<chromeos::DBusMethodCallback<lorgnette::ListScannersResponse>>
        session_end_callback;
    std::optional<
        base::RepeatingCallback<void(lorgnette::ScannerListChangedSignal)>>
        signal_callback;
    std::string session_id;
    lorgnette::ListScannersResponse response;
    base::TimeTicks last_event;
    base::TimeDelta max_event_interval;
  };

  // Helper function to send a GetNextImage request to lorgnette for the scan
  // job with the given UUID.
  // Requires that scan_job_state_ contains uuid.
  void GetNextImage(const std::string& uuid) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    lorgnette::GetNextImageRequest request;
    request.set_scan_uuid(uuid);

    ScanJobState& state = scan_job_state_.at(uuid);

    dbus::MethodCall method_call(lorgnette::kManagerServiceInterface,
                                 lorgnette::kGetNextImageMethod);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode GetNextImageRequest protobuf";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(state.completion_callback),
                                    lorgnette::SCAN_FAILURE_MODE_UNKNOWN));
      scan_job_state_.erase(uuid);
      return;
    }

    auto scan_data_reader = std::make_unique<ScanDataReader>();
    base::ScopedFD fd = scan_data_reader->Start();
    writer.AppendFileDescriptor(fd.get());

    state.scan_data_reader = std::move(scan_data_reader);

    lorgnette_daemon_proxy_->CallMethod(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&LorgnetteManagerClientImpl::OnGetNextImageResponse,
                       weak_ptr_factory_.GetWeakPtr(), uuid));
  }

  // Helper method to actually perform scan cancellation.
  // We use this method since the scan cancel logic requires that we are running
  // on the proper sequence.
  void DoScanCancel(chromeos::VoidDBusMethodCallback cancel_callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (scan_job_state_.size() == 0) {
      LOG(ERROR) << "No active scan job to cancel.";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(cancel_callback), false));
      return;
    }

    // A more robust implementation would pass a scan job identifier to callers
    // of StartScan() so they could request cancellation of a particular scan.
    if (scan_job_state_.size() > 1) {
      LOG(ERROR) << "Multiple scan jobs running; not clear which to cancel.";
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(cancel_callback), false));
      return;
    }

    std::string uuid = scan_job_state_.begin()->first;

    lorgnette::CancelScanRequest request;
    request.set_scan_uuid(uuid);

    ScanJobState& state = scan_job_state_.begin()->second;
    state.cancel_callback = std::move(cancel_callback);

    CancelScan(request,
               base::BindOnce(&LorgnetteManagerClientImpl::OnCancelScanResponse,
                              weak_ptr_factory_.GetWeakPtr(), uuid));
  }

  // Handles the response received after calling GetScannerCapabilities().
  void OnScannerCapabilitiesResponse(
      chromeos::DBusMethodCallback<lorgnette::ScannerCapabilities> callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain ScannerCapabilities";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::ScannerCapabilities response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to read ScannerCapabilities";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(std::move(response_proto));
  }

  // Handles the response received after calling OpenScanner().
  void OnOpenScannerResponse(
      chromeos::DBusMethodCallback<lorgnette::OpenScannerResponse> callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain OpenScannerResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::OpenScannerResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode OpenScannerResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  // Handles the response received after calling CloseScanner().
  void OnCloseScannerResponse(
      chromeos::DBusMethodCallback<lorgnette::CloseScannerResponse> callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain CloseScannerResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::CloseScannerResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode CloseScannerResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  // Handles the response received after calling SetOptions().
  void OnSetOptionsResponse(
      chromeos::DBusMethodCallback<lorgnette::SetOptionsResponse> callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain SetOptionsResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::SetOptionsResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode SetOptionsResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  // Handles the response received after calling GetCurrentConfig().
  void OnGetCurrentConfigResponse(
      chromeos::DBusMethodCallback<lorgnette::GetCurrentConfigResponse>
          callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain GetCurrentConfigResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::GetCurrentConfigResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode GetCurrentConfigResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  // Handles the response received after calling StartPreparedScan.
  void OnStartPreparedScanResponse(
      chromeos::DBusMethodCallback<lorgnette::StartPreparedScanResponse>
          callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain StartPreparedScanResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::StartPreparedScanResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode StartPreparedScanResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  // Called when scan data read is completed.
  void OnScanDataCompleted(const std::string& uuid,
                           uint32_t page_number,
                           bool more_pages,
                           std::optional<std::string> data) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!base::Contains(scan_job_state_, uuid)) {
      LOG(ERROR) << "Received ScanDataCompleted for unrecognized scan job: "
                 << uuid;
      return;
    }

    ScanJobState& state = scan_job_state_[uuid];
    if (!data.has_value()) {
      LOG(ERROR) << "Reading scan data failed";
      std::move(state.completion_callback)
          .Run(lorgnette::SCAN_FAILURE_MODE_UNKNOWN);
      scan_job_state_.erase(uuid);
      return;
    }

    state.page_callback.Run(std::move(data.value()), page_number);

    if (more_pages) {
      GetNextImage(uuid);
    } else {
      std::move(state.completion_callback)
          .Run(lorgnette::SCAN_FAILURE_MODE_NO_FAILURE);
      scan_job_state_.erase(uuid);
    }
  }

  void OnStartScanResponse(ScanJobState state, dbus::Response* response) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!response) {
      LOG(ERROR) << "Failed to obtain StartScanResponse";
      std::move(state.completion_callback)
          .Run(lorgnette::SCAN_FAILURE_MODE_UNKNOWN);
      return;
    }

    lorgnette::StartScanResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode StartScanResponse proto";
      std::move(state.completion_callback)
          .Run(lorgnette::SCAN_FAILURE_MODE_UNKNOWN);
      return;
    }

    if (response_proto.state() == lorgnette::SCAN_STATE_FAILED) {
      LOG(ERROR) << "Starting Scan failed: " << response_proto.failure_reason();
      std::move(state.completion_callback)
          .Run(response_proto.scan_failure_mode());
      return;
    }

    scan_job_state_[response_proto.scan_uuid()] = std::move(state);
    GetNextImage(response_proto.scan_uuid());
  }

  // Handles the response received after calling ReadScanData().
  void OnReadScanDataResponse(
      chromeos::DBusMethodCallback<lorgnette::ReadScanDataResponse> callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain ReadScanDataResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::ReadScanDataResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode ReadScanDataResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  void OnCancelScanResponse(
      const std::string& scan_uuid,
      std::optional<lorgnette::CancelScanResponse> response) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    // If the cancel completed and the scan job has been erased, there's no work
    // to do.
    auto it = scan_job_state_.find(scan_uuid);
    if (it == scan_job_state_.end())
      return;

    ScanJobState& state = it->second;
    if (state.cancel_callback.is_null()) {
      LOG(ERROR) << "No callback active to cancel job " << scan_uuid;
      return;
    }
    if (!response) {
      LOG(ERROR) << "Failed to obtain CancelScanResponse";
      std::move(state.cancel_callback).Run(false);
      return;
    }

    // If the cancel request failed, report the cancel as failed via the
    // callback. Otherwise, wait for the cancel to complete.
    if (!response->success()) {
      LOG(ERROR) << "Cancelling scan failed: " << response->failure_reason();
      std::move(state.cancel_callback).Run(false);
      return;
    }
  }

  // Handles the response received after calling CancelScan with a
  // CancelScanRequest.
  void OnCancelScanJobResponse(
      chromeos::DBusMethodCallback<lorgnette::CancelScanResponse> callback,
      dbus::Response* response) {
    if (!response) {
      LOG(ERROR) << "Failed to obtain CancelScanResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::CancelScanResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode CancelScanResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    std::move(callback).Run(response_proto);
  }

  // Called when a response to a GetNextImage request is received from
  // lorgnette. Handles stopping the scan if the request failed.
  void OnGetNextImageResponse(std::string uuid, dbus::Response* response) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    // If the scan was canceled and the scan job has been erased, there's no
    // need to check the next image response.
    auto it = scan_job_state_.find(uuid);
    if (it == scan_job_state_.end())
      return;

    ScanJobState& state = it->second;
    if (!response) {
      LOG(ERROR) << "Failed to obtain GetNextImage response";
      std::move(state.completion_callback)
          .Run(lorgnette::SCAN_FAILURE_MODE_UNKNOWN);
      scan_job_state_.erase(uuid);
      return;
    }

    lorgnette::GetNextImageResponse response_proto;
    dbus::MessageReader reader(response);
    if (!reader.PopArrayOfBytesAsProto(&response_proto)) {
      LOG(ERROR) << "Failed to decode GetNextImageResponse proto";
      std::move(state.completion_callback)
          .Run(lorgnette::SCAN_FAILURE_MODE_UNKNOWN);
      scan_job_state_.erase(uuid);
      return;
    }

    if (!response_proto.success()) {
      LOG(ERROR) << "Getting next image failed: "
                 << response_proto.failure_reason();
      std::move(state.completion_callback)
          .Run(response_proto.scan_failure_mode());
      scan_job_state_.erase(uuid);
      return;
    }
  }

  void OnStartScannerDiscoveryResponse(
      const lorgnette::StartScannerDiscoveryRequest& request,
      base::RepeatingCallback<void(lorgnette::ScannerListChangedSignal)>
          signal_callback,
      chromeos::DBusMethodCallback<lorgnette::StartScannerDiscoveryResponse>
          response_callback,
      dbus::Response* dbus_response) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!dbus_response) {
      LOG(ERROR) << "Failed to obtain StartScannerDiscoveryResponse";
      std::move(response_callback).Run(std::nullopt);
      return;
    }

    lorgnette::StartScannerDiscoveryResponse response;
    dbus::MessageReader reader(dbus_response);
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to decode StartScannerDiscoveryResponse proto";
      std::move(response_callback).Run(std::nullopt);
      return;
    }

    if (!response.started()) {
      LOG(ERROR) << "Scanner discovery session was not started";
      std::move(response_callback).Run(std::nullopt);
      return;
    }

    DiscoverySessionState session;
    session.session_id = response.session_id();
    session.signal_callback = std::move(signal_callback);
    session.last_event = base::TimeTicks::Now();
    session.max_event_interval = base::Milliseconds(0);
    discovery_sessions_[response.session_id()] = std::move(session);
    PRINTER_LOG(DEBUG) << "Client " << request.client_id()
                       << " started discovery session: "
                       << response.session_id();

    if (discovery_sessions_.size() == 1) {
      // Passing "this" is safe because discovery_monitor_ will be destroyed
      // before this object.
      discovery_monitor_.Start(
          FROM_HERE, kDiscoveryMonitorInterval, this,
          &LorgnetteManagerClientImpl::CheckDiscoverySessions);
    }
    std::move(response_callback).Run(std::move(response));
  }

  void CheckDiscoverySessions() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    base::TimeTicks now = base::TimeTicks::Now();
    std::vector<std::string> expired_sessions;
    // Find all of the sessions that should expire due to inactivity.  Delete
    // them in a separate loop because the SESSION_ENDING handler may delete the
    // session from discovery_sessions_.
    for (auto& [id, session] : discovery_sessions_) {
      base::TimeDelta inactive_duration = now - session.last_event;
      if (inactive_duration > kDiscoveryMaxInactivity) {
        expired_sessions.emplace_back(id);
        session.response.set_result(lorgnette::OPERATION_RESULT_CANCELLED);
      }
    }
    for (const std::string& id : expired_sessions) {
      DiscoverySessionState& session = discovery_sessions_.at(id);
      PRINTER_LOG(EVENT) << "Terminating idle discovery session " << id;
      lorgnette::ScannerListChangedSignal signal;
      signal.set_session_id(id);
      signal.set_event_type(
          lorgnette::ScannerListChangedSignal::SESSION_ENDING);
      if (session.signal_callback) {
        session.signal_callback->Run(std::move(signal));
      }
    }
  }

  void OnListScannersDiscoverySession(
      chromeos::DBusMethodCallback<lorgnette::ListScannersResponse> callback,
      std::optional<lorgnette::StartScannerDiscoveryResponse> response) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    if (!response.has_value()) {
      // TODO(b/277049005): Set the proper result code once this is disentangled
      // from the synchronous ListScanners response.
      std::move(callback).Run(std::nullopt);
      return;
    }

    // This will have been created already by OnStartScannerDiscoveryResponse,
    // so no need to search for it first.
    DCHECK(base::Contains(discovery_sessions_, response->session_id()));
    discovery_sessions_[response->session_id()].session_end_callback =
        std::move(callback);
  }

  void OnStopScannerDiscoveryResponse(
      std::string session_id,
      chromeos::DBusMethodCallback<lorgnette::StopScannerDiscoveryResponse>
          callback,
      dbus::Response* dbus_response) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!dbus_response) {
      LOG(ERROR) << "Failed to obtain StopScannerDiscoveryResponse";
      std::move(callback).Run(std::nullopt);
      return;
    }

    lorgnette::StopScannerDiscoveryResponse response;
    dbus::MessageReader reader(dbus_response);
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to decode StopScannerDiscoveryResponse proto";
      std::move(callback).Run(std::nullopt);
      return;
    }

    PRINTER_LOG(DEBUG) << "Scanner discovery session " << session_id
                       << (response.stopped() ? " was " : " was not ")
                       << "stopped.";
    std::move(callback).Run(response);
  }

  void ScanStatusChangedReceived(dbus::Signal* signal) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    dbus::MessageReader reader(signal);
    lorgnette::ScanStatusChangedSignal signal_proto;
    if (!reader.PopArrayOfBytesAsProto(&signal_proto)) {
      LOG(ERROR) << "Failed to decode ScanStatusChangedSignal proto";
      return;
    }

    if (!base::Contains(scan_job_state_, signal_proto.scan_uuid())) {
      LOG(ERROR) << "Received signal for unrecognized scan job: "
                 << signal_proto.scan_uuid();
      return;
    }
    ScanJobState& state = scan_job_state_[signal_proto.scan_uuid()];

    if (signal_proto.state() == lorgnette::SCAN_STATE_FAILED) {
      LOG(ERROR) << "Scan job " << signal_proto.scan_uuid()
                 << " failed: " << signal_proto.failure_reason();
      std::move(state.completion_callback)
          .Run(signal_proto.scan_failure_mode());
      scan_job_state_.erase(signal_proto.scan_uuid());
    } else if (signal_proto.state() == lorgnette::SCAN_STATE_PAGE_COMPLETED) {
      VLOG(1) << "Scan job " << signal_proto.scan_uuid() << " page "
              << signal_proto.page() << " completed successfully";
      ScanDataReader* scan_data_reader = state.scan_data_reader.get();
      scan_data_reader->Wait(base::BindOnce(
          &LorgnetteManagerClientImpl::OnScanDataCompleted,
          weak_ptr_factory_.GetWeakPtr(), signal_proto.scan_uuid(),
          signal_proto.page(), signal_proto.more_pages()));
    } else if (signal_proto.state() == lorgnette::SCAN_STATE_COMPLETED) {
      VLOG(1) << "Scan job " << signal_proto.scan_uuid()
              << " completed successfully";
    } else if (signal_proto.state() == lorgnette::SCAN_STATE_IN_PROGRESS &&
               !state.progress_callback.is_null()) {
      state.progress_callback.Run(signal_proto.progress(), signal_proto.page());
    } else if (signal_proto.state() == lorgnette::SCAN_STATE_CANCELLED) {
      VLOG(1) << "Scan job " << signal_proto.scan_uuid()
              << " has been cancelled.";
      if (!state.cancel_callback.is_null())
        std::move(state.cancel_callback).Run(true);

      scan_job_state_.erase(signal_proto.scan_uuid());
    }
  }

  void ScannerListChangedReceived(dbus::Signal* dbus_signal) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    dbus::MessageReader reader(dbus_signal);
    lorgnette::ScannerListChangedSignal signal;
    if (!reader.PopArrayOfBytesAsProto(&signal)) {
      LOG(ERROR) << "Failed to decode ScannerListChangedSignal proto";
      return;
    }

    if (!base::Contains(discovery_sessions_, signal.session_id())) {
      LOG(ERROR) << "Received signal for unrecognized discovery session: "
                 << signal.session_id();
      return;
    }
    DiscoverySessionState& session = discovery_sessions_[signal.session_id()];

    if (!session.signal_callback) {
      LOG(WARNING) << "Scanner discovery session " << signal.session_id()
                   << " does not have a signal handler registered";
      return;
    }
    session.signal_callback->Run(std::move(signal));
  }

  void ListScannersDiscoveryScannersUpdated(
      lorgnette::ScannerListChangedSignal signal) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    DCHECK(base::Contains(discovery_sessions_, signal.session_id()));
    DiscoverySessionState& session = discovery_sessions_[signal.session_id()];
    base::TimeDelta event_interval =
        base::TimeTicks::Now() - session.last_event;
    if (event_interval > session.max_event_interval) {
      session.max_event_interval = event_interval;
    }

    switch (signal.event_type()) {
      case lorgnette::ScannerListChangedSignal::SCANNER_ADDED:
        PRINTER_LOG(EVENT) << "Discovered SANE scanner: "
                           << signal.scanner().name();
        session.last_event = base::TimeTicks::Now();
        *session.response.add_scanners() = std::move(signal.scanner());
        break;
      case lorgnette::ScannerListChangedSignal::SCANNER_REMOVED:
        // TODO(b/303855027): Once this is implemented in the backend, this
        // needs to be updated to actually remove devices.
        session.last_event = base::TimeTicks::Now();
        break;
      case lorgnette::ScannerListChangedSignal::ENUM_COMPLETE: {
        PRINTER_LOG(EVENT) << "Enumeration completed for discovery session: "
                           << session.session_id;
        session.response.set_result(lorgnette::OPERATION_RESULT_SUCCESS);
        session.last_event = base::TimeTicks::Now();
        lorgnette::StopScannerDiscoveryRequest request;
        request.set_session_id(session.session_id);
        StopScannerDiscovery(request, base::DoNothing());
        break;
      }
      case lorgnette::ScannerListChangedSignal::SESSION_ENDING:
        PRINTER_LOG(EVENT) << "Session ending for discovery session: "
                           << session.session_id;
        base::UmaHistogramCounts100("Scanning.DiscoverySession.NumScanners",
                                    session.response.scanners_size());
        base::UmaHistogramEnumeration(
            "Scanning.DiscoverySession.Result", session.response.result(),
            static_cast<lorgnette::OperationResult>(
                lorgnette::OperationResult_ARRAYSIZE));
        base::UmaHistogramMediumTimes("Scanning.DiscoverySession.MaxInterval",
                                      session.max_event_interval);
        DCHECK(session.session_end_callback);
        std::move(*session.session_end_callback)
            .Run(std::move(session.response));
        discovery_sessions_.erase(session.session_id);
        break;
      default:
        NOTREACHED_IN_MIGRATION();
    }

    if (discovery_sessions_.size() == 0) {
      discovery_monitor_.Stop();
    }
  }

  void LorgnetteSignalConnected(const std::string& interface_name,
                                const std::string& signal_name,
                                bool success) {
    LOG_IF(WARNING, !success)
        << "Failed to connect to lorgnette " << interface_name
        << "::" << signal_name << " signal.";
  }

  raw_ptr<dbus::ObjectProxy> lorgnette_daemon_proxy_ = nullptr;

  // Map from scan UUIDs to ScanDataReader and callbacks for reporting scan
  // progress and completion.
  base::flat_map<std::string, ScanJobState> scan_job_state_
      GUARDED_BY_CONTEXT(sequence_checker_);
  // Map from discovery session IDs to a DiscoverySessionState tracking the
  // state and callbacks for that session.
  base::flat_map<std::string, DiscoverySessionState> discovery_sessions_
      GUARDED_BY_CONTEXT(sequence_checker_);
  // Ensures that all callbacks are handled on the same sequence, so that it is
  // safe to access scan_job_state_ and discovery_sessions_ without a lock.
  SEQUENCE_CHECKER(sequence_checker_);
  // Triggers a recurring check for hung discovery sessions when discovery is
  // active.
  base::RepeatingTimer discovery_monitor_;

  base::WeakPtrFactory<LorgnetteManagerClientImpl> weak_ptr_factory_{this};
};

}  // namespace

// static
void LorgnetteManagerClient::Initialize(dbus::Bus* bus) {
  CHECK(bus);
  (new LorgnetteManagerClientImpl())->Init(bus);
}

// static
void LorgnetteManagerClient::InitializeFake() {
  new FakeLorgnetteManagerClient();
}

// static
void LorgnetteManagerClient::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
}

// static
LorgnetteManagerClient* LorgnetteManagerClient::Get() {
  return g_instance;
}

LorgnetteManagerClient::LorgnetteManagerClient() {
  CHECK(!g_instance);
  g_instance = this;
}

LorgnetteManagerClient::~LorgnetteManagerClient() {
  CHECK_EQ(g_instance, this);
  g_instance = nullptr;
}

}  // namespace ash