chromium/chrome/browser/extensions/api/document_scan/document_scan_api_handler.h

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

#ifndef CHROME_BROWSER_EXTENSIONS_API_DOCUMENT_SCAN_DOCUMENT_SCAN_API_HANDLER_H_
#define CHROME_BROWSER_EXTENSIONS_API_DOCUMENT_SCAN_DOCUMENT_SCAN_API_HANDLER_H_

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "chrome/common/extensions/api/document_scan.h"
#include "chromeos/crosapi/mojom/document_scan.mojom-forward.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/browser_context_keyed_api_factory.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
#include "extensions/common/extension_id.h"
#include "ui/gfx/native_widget_types.h"

class PrefRegistrySimple;

namespace content {
class BrowserContext;
}  // namespace content

namespace gfx {
class Image;
}  // namespace gfx

namespace extensions {

class Extension;
class ScannerDiscoveryRunner;
class StartScanRunner;

// Handles chrome.documentScan API function calls.
class DocumentScanAPIHandler : public BrowserContextKeyedAPI,
                               public ExtensionRegistryObserver {
 public:
  using SimpleScanCallback = base::OnceCallback<void(
      std::optional<api::document_scan::ScanResults> scan_results,
      std::optional<std::string> error)>;
  using GetScannerListCallback =
      base::OnceCallback<void(api::document_scan::GetScannerListResponse)>;
  using OpenScannerCallback =
      base::OnceCallback<void(api::document_scan::OpenScannerResponse)>;
  using GetOptionGroupsCallback =
      base::OnceCallback<void(api::document_scan::GetOptionGroupsResponse)>;
  using CloseScannerCallback =
      base::OnceCallback<void(api::document_scan::CloseScannerResponse)>;
  using SetOptionsCallback =
      base::OnceCallback<void(api::document_scan::SetOptionsResponse)>;
  using StartScanCallback =
      base::OnceCallback<void(api::document_scan::StartScanResponse)>;
  using CancelScanCallback =
      base::OnceCallback<void(api::document_scan::CancelScanResponse)>;
  using ReadScanDataCallback =
      base::OnceCallback<void(api::document_scan::ReadScanDataResponse)>;

  static std::unique_ptr<DocumentScanAPIHandler> CreateForTesting(
      content::BrowserContext* browser_context,
      crosapi::mojom::DocumentScan* document_scan);

  explicit DocumentScanAPIHandler(content::BrowserContext* browser_context);
  DocumentScanAPIHandler(const DocumentScanAPIHandler&) = delete;
  DocumentScanAPIHandler& operator=(const DocumentScanAPIHandler&) = delete;
  ~DocumentScanAPIHandler() override;

  // BrowserContextKeyedAPI:
  static BrowserContextKeyedAPIFactory<DocumentScanAPIHandler>*
  GetFactoryInstance();

  // Returns the current instance for `browser_context`.
  static DocumentScanAPIHandler* Get(content::BrowserContext* browser_context);

  // Registers the documentScan API preference with the |registry|.
  static void RegisterProfilePrefs(PrefRegistrySimple* registry);

  // ExtensionRegistryObserver implementation:
  void OnExtensionUnloaded(content::BrowserContext* browser_context,
                           const Extension* extension,
                           UnloadedExtensionReason reason) override;

  // KeyedService implementation:
  void Shutdown() override;

  // Replaces the DocumentScan service with a mock.
  void SetDocumentScanForTesting(crosapi::mojom::DocumentScan* document_scan);

  // Scans one page from the first available scanner on the system and passes
  // the result to `callback`.  `mime_types` is a list of MIME types the caller
  // is willing to receive back as the image format.
  void SimpleScan(const std::vector<std::string>& mime_types,
                  SimpleScanCallback callback);

  // If the user approves, gets a list of available scanners that match
  // `filter`.  Explicit approval is obtained through a Chrome dialog or by
  // adding the extension ID to the list of trusted document scan extensions.
  // `user_gesture` indicates whether the scan was initiated by a user action
  // and should be passed as the result of `ExtensionFunction::user_gesture()`.
  // The result of the denial or the backend call will be passed to `callback`.
  // Note that scanner and job handles previously issued by the backend will
  // become invalid after calling this function.
  void GetScannerList(gfx::NativeWindow native_window,
                      scoped_refptr<const Extension> extension,
                      bool user_gesture,
                      api::document_scan::DeviceFilter filter,
                      GetScannerListCallback callback);

  // Given `scanner_id` previously returned from `GetScannerList`, opens the
  // device for exclusive access.  The result containing a handle and the set of
  // current device options will be passed to `callback`.
  // Note that job and scanner handles previously returned by the backend for
  // the same `scanner_id` will automatically be closed.
  void OpenScanner(scoped_refptr<const Extension> extension,
                   const std::string& scanner_id,
                   OpenScannerCallback callback);

  // Given `scanner_handle` previously returned from `OpenScanner`, gets the
  // group names and member options for that scanner.  The result will be passed
  // to `callback`.
  void GetOptionGroups(scoped_refptr<const Extension> extension,
                       const std::string& scanner_handle,
                       GetOptionGroupsCallback callback);

  // Given `scanner_handle` previously returned from `OpenScanner`, closes the
  // handle.  No further operations on this handle can be performed even if the
  // result code does not indicate success.  The result of closing the handle on
  // the backend will be passed to `callback`.
  void CloseScanner(scoped_refptr<const Extension> extension,
                    const std::string& scanner_handle,
                    CloseScannerCallback callback);

  // Given `scanner_handle` previously returned from `OpenScanner`, sends the
  // list of new option values in `options` to the backend.  The backend will
  // attempt to set each option in order, then will respond with a result for
  // each operation and a new final set of device options.  The full response
  // will be passed to `callback`.
  void SetOptions(scoped_refptr<const Extension> extension,
                  const std::string& scanner_handle,
                  const std::vector<api::document_scan::OptionSetting>& options,
                  SetOptionsCallback callback);

  // If the user approves, starts a scan using scanner options previously
  // configured via `SetOptions`.  Additionally, `options` are used to specify
  // scanner-framework options.  Explicit approval is obtained through a Chrome
  // dialog or by adding the extension ID to the list of trusted document scan
  // extensions.  `user_gesture` indicates whether the scan was initiated by a
  // user action and should be passed as the result of
  // `ExtensionFunction::user_gesture()`. The result of the denial or the
  // backend call will be passed to `callback`.
  void StartScan(gfx::NativeWindow native_window,
                 scoped_refptr<const Extension> extension,
                 bool user_gesture,
                 const std::string& scanner_handle,
                 api::document_scan::StartScanOptions options,
                 StartScanCallback callback);

  // Cancels a scan using a `job_handle` that was returned from `StartScan` and
  // passes the result to `callback`.
  void CancelScan(scoped_refptr<const Extension> extension,
                  const std::string& job_handle,
                  CancelScanCallback callback);

  // Given `job_handle` previously returned from `StartScan`, requests the next
  // available chunk of scanned image data.  The result from the backend will be
  // passed to `callback`.
  void ReadScanData(scoped_refptr<const Extension> extension,
                    const std::string& job_handle,
                    ReadScanDataCallback callback);

 private:
  // Needed for BrowserContextKeyedAPI implementation.
  friend class BrowserContextKeyedAPIFactory<DocumentScanAPIHandler>;

  // Info that relates to a physical scanner.
  struct ScannerDevice {
    // The string used on the backend to connect to a scanner.
    std::string connection_string;

    // The name of a scanner.
    std::string name;
  };

  // Tracks open handles and scanner IDs that have been given out to a
  // particular extension.  These are the things this has to track for
  // correctness.  For everything else the source of truth is maintained in the
  // backend.
  struct ExtensionState {
    ExtensionState();
    ~ExtensionState();

    // Map from scanner IDs returned from the most recent call to
    // GetScannerList() to their matching device info.  Attempting to open any
    // scanner ID not in this set will fail.
    std::map<std::string, ScannerDevice> active_scanner_ids;

    // Map from scanner handle to scanner's ID (the latter can be used to look
    // up scanner in `active_scanner_ids`).
    std::map<std::string, std::string> scanner_handles;

    // Map from active job handles back to the originating scanner handle.
    std::map<std::string, std::string> active_job_handles;

    // A set of scanner IDs the user has approved for scanning.  These can be
    // used to start new scan jobs from actions triggered by a user gesture.
    std::set<std::string> approved_scanner_ids;

    // A set of scanner handles the user has approved for scanning.  These can
    // be used to start new scan jobs until the handles are closed.
    std::set<std::string> approved_scanner_handles;

    // Whether the user has confirmed that this extension is allowed to discover
    // scanners.
    bool discovery_approved;
  };

  // BrowserContextKeyedAPI:
  static const char* service_name() { return "DocumentScanAPIHandler"; }
  static const bool kServiceIsCreatedWithBrowserContext = false;
  static const bool kServiceIsNULLWhileTesting = true;
  static const bool kServiceHasOwnInstanceInIncognito = true;

  // Used by CreateForTesting:
  DocumentScanAPIHandler(content::BrowserContext* browser_context,
                         crosapi::mojom::DocumentScan* document_scan);

  // Cleanup all handles and state for the given extension.
  void ExtensionCleanup(const ExtensionId& id);

  void OnSimpleScanNamesReceived(bool force_virtual_usb_printer,
                                 SimpleScanCallback callback,
                                 const std::vector<std::string>& scanner_names);
  void OnSimpleScanCompleted(SimpleScanCallback callback,
                             crosapi::mojom::ScanFailureMode failure_mode,
                             const std::optional<std::string>& scan_data);

  void SendGetScannerListRequest(const api::document_scan::DeviceFilter& filter,
                                 GetScannerListCallback callback);
  void ShowScanDiscoveryDialog(const api::document_scan::DeviceFilter& filter,
                               GetScannerListCallback callback,
                               const gfx::Image& icon);
  void OnScannerListReceived(
      std::unique_ptr<ScannerDiscoveryRunner> discovery_runner,
      GetScannerListCallback callback,
      crosapi::mojom::GetScannerListResponsePtr response);
  void OnOpenScannerResponse(const ExtensionId& extension_id,
                             const std::string& scanner_id,
                             OpenScannerCallback callback,
                             crosapi::mojom::OpenScannerResponsePtr response);
  void OnGetOptionGroupsResponse(
      GetOptionGroupsCallback callback,
      crosapi::mojom::GetOptionGroupsResponsePtr response);
  void OnCloseScannerResponse(const ExtensionId& extension_id,
                              CloseScannerCallback callback,
                              crosapi::mojom::CloseScannerResponsePtr response);
  void OnSetOptionsResponse(SetOptionsCallback callback,
                            crosapi::mojom::SetOptionsResponsePtr response);
  void OnStartScanResponse(
      std::unique_ptr<StartScanRunner> runner,
      StartScanCallback callback,
      crosapi::mojom::StartPreparedScanResponsePtr response);
  void OnCancelScanResponse(const ExtensionId& extension_id,
                            CancelScanCallback callback,
                            crosapi::mojom::CancelScanResponsePtr response);
  void OnReadScanDataResponse(ReadScanDataCallback callback,
                              crosapi::mojom::ReadScanDataResponsePtr response);

  raw_ptr<content::BrowserContext> browser_context_;
  raw_ptr<crosapi::mojom::DocumentScan> document_scan_;
  std::map<ExtensionId, ExtensionState> extension_state_;

  base::ScopedObservation<ExtensionRegistry, ExtensionRegistryObserver>
      extension_registry_observation_{this};

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

template <>
KeyedService*
BrowserContextKeyedAPIFactory<DocumentScanAPIHandler>::BuildServiceInstanceFor(
    content::BrowserContext* context) const;

}  // namespace extensions

#endif  // CHROME_BROWSER_EXTENSIONS_API_DOCUMENT_SCAN_DOCUMENT_SCAN_API_HANDLER_H_