chromium/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.cc

// Copyright 2020 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/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.h"

#include <memory>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/webui/camera_app_ui/ocr.mojom.h"
#include "ash/webui/camera_app_ui/pdf_builder.mojom.h"
#include "ash/webui/camera_app_ui/url_constants.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/memory/scoped_refptr.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/system_web_apps/apps/camera_app/camera_app_survey_handler.h"
#include "chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_constants.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/feedback/show_feedback_page.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_device_salt_service_factory.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/pdf/pdf_service.h"
#include "chrome/browser/policy/policy_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/screen_ai/public/optical_character_recognizer.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/webui/ash/internet_config_dialog.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_launch_queue.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/common/pref_names.h"
#include "chrome/services/pdf/public/mojom/pdf_progressive_searchifier.mojom.h"
#include "chrome/services/pdf/public/mojom/pdf_service.mojom.h"
#include "chrome/services/pdf/public/mojom/pdf_thumbnailer.mojom.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/devicetype.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui_data_source.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/screen_ai/public/mojom/screen_ai_service.mojom.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/chromeos/styles/cros_styles.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/native_widget_types.h"
#include "url/gurl.h"

namespace {

using SecurityType = chromeos::network_config::mojom::SecurityType;

std::string DeviceTypeToString(chromeos::DeviceType device_type) {
  switch (device_type) {
    case chromeos::DeviceType::kChromebase:
      return "chromebase";
    case chromeos::DeviceType::kChromebit:
      return "chromebit";
    case chromeos::DeviceType::kChromebook:
      return "chromebook";
    case chromeos::DeviceType::kChromebox:
      return "chromebox";
    case chromeos::DeviceType::kUnknown:
    default:
      return "unknown";
  }
}
const int64_t kStorageLowThreshold = 128 * 1024 * 1024;           // 128MB
const int64_t kStorageCriticallyLowThreshold = 32 * 1024 * 1024;  // 32MB

// PDFs saved from CCA are always 72 dpi.
constexpr int kPdfDpi = 72;

}  // namespace

// static
void ChromeCameraAppUIDelegate::CameraAppDialog::ShowIntent(
    const std::string& queries,
    gfx::NativeWindow parent) {
  std::string url = ash::kChromeUICameraAppMainURL + queries;
  CameraAppDialog* dialog = new CameraAppDialog(url);
  dialog->ShowSystemDialog(parent);
}

ChromeCameraAppUIDelegate::CameraAppDialog::CameraAppDialog(
    const std::string& url)
    : ash::SystemWebDialogDelegate(GURL(url),
                                   /*title=*/std::u16string()) {
  set_can_maximize(true);
  // For customizing the title bar.
  set_dialog_frame_kind(ui::WebDialogDelegate::FrameKind::kNonClient);
  set_dialog_modal_type(ui::mojom::ModalType::kWindow);
  set_dialog_size(
      gfx::Size(kChromeCameraAppDefaultWidth, kChromeCameraAppDefaultHeight));
}

ChromeCameraAppUIDelegate::CameraAppDialog::~CameraAppDialog() = default;

void ChromeCameraAppUIDelegate::CameraAppDialog::AdjustWidgetInitParams(
    views::Widget::InitParams* params) {
  auto grey_900 = cros_styles::ResolveColor(
      cros_styles::ColorName::kGoogleGrey900, /*is_dark_mode=*/false,
      /*use_debug_colors=*/false);
  params->init_properties_container.SetProperty(
      chromeos::kTrackDefaultFrameColors, false);
  params->init_properties_container.SetProperty(chromeos::kFrameActiveColorKey,
                                                grey_900);
  params->init_properties_container.SetProperty(
      chromeos::kFrameInactiveColorKey, grey_900);
}

void ChromeCameraAppUIDelegate::CameraAppDialog::RequestMediaAccessPermission(
    content::WebContents* web_contents,
    const content::MediaStreamRequest& request,
    content::MediaResponseCallback callback) {
  MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest(
      web_contents, request, std::move(callback), /* extension */ nullptr);
}

bool ChromeCameraAppUIDelegate::CameraAppDialog::CheckMediaAccessPermission(
    content::RenderFrameHost* render_frame_host,
    const url::Origin& security_origin,
    blink::mojom::MediaStreamType type) {
  return MediaCaptureDevicesDispatcher::GetInstance()
      ->CheckMediaAccessPermission(render_frame_host, security_origin, type);
}

ChromeCameraAppUIDelegate::FileMonitor::FileMonitor() = default;

ChromeCameraAppUIDelegate::FileMonitor::~FileMonitor() = default;

void ChromeCameraAppUIDelegate::FileMonitor::Monitor(
    const base::FilePath& file_path,
    base::OnceCallback<void(FileMonitorResult)> callback) {
  // Cancel the previous monitor callback if it hasn't been notified.
  if (!callback_.is_null()) {
    std::move(callback_).Run(FileMonitorResult::kCanceled);
  }

  // There is chance that the file is deleted during the task is scheduled and
  // executed. Therefore, check here before watching it.
  if (!base::PathExists(file_path)) {
    std::move(callback).Run(FileMonitorResult::kDeleted);
    return;
  }

  callback_ = std::move(callback);
  file_watcher_ = std::make_unique<base::FilePathWatcher>();
  if (!file_watcher_->Watch(
          file_path, base::FilePathWatcher::Type::kNonRecursive,
          base::BindRepeating(
              &ChromeCameraAppUIDelegate::FileMonitor::OnFileDeletion,
              base::Unretained(this)))) {
    std::move(callback_).Run(FileMonitorResult::kError);
  }
}

void ChromeCameraAppUIDelegate::FileMonitor::OnFileDeletion(
    const base::FilePath& path,
    bool error) {
  if (callback_.is_null()) {
    return;
  }

  if (error) {
    std::move(callback_).Run(FileMonitorResult::kError);
    return;
  }
  std::move(callback_).Run(FileMonitorResult::kDeleted);
}

ChromeCameraAppUIDelegate::StorageMonitor::StorageMonitor(
    scoped_refptr<base::SequencedTaskRunner> task_runner)
    : task_runner_(task_runner) {}

ChromeCameraAppUIDelegate::StorageMonitor::~StorageMonitor() {
  if (timer_.IsRunning()) {
    StopMonitoring();
  }
}

void ChromeCameraAppUIDelegate::StorageMonitor::StartMonitoring(
    base::FilePath monitor_path,
    base::RepeatingCallback<void(StorageMonitorStatus)> callback) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  // Initialize and set most properties
  monitor_path_ = std::move(monitor_path);
  callback_ = callback;

  // Get initial status.
  status_ = GetCurrentStatus();
  callback_.Run(status_);

  // Set the timer to monitor status changes.
  timer_.Start(
      FROM_HERE, base::Seconds(5), this,
      &ChromeCameraAppUIDelegate::StorageMonitor::MonitorCurrentStatus);
}

void ChromeCameraAppUIDelegate::StorageMonitor::StopMonitoring() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  if (timer_.IsRunning()) {
    timer_.Stop();
  }
}

base::WeakPtr<ChromeCameraAppUIDelegate::StorageMonitor>
ChromeCameraAppUIDelegate::StorageMonitor::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

ChromeCameraAppUIDelegate::StorageMonitorStatus
ChromeCameraAppUIDelegate::StorageMonitor::GetCurrentStatus() {
  auto current_storage = base::SysInfo::AmountOfFreeDiskSpace(monitor_path_);
  auto status = StorageMonitorStatus::kNormal;
  if (current_storage < 0) {
    LOG(ERROR) << "Failed to get the amount of free disk space.";
    status = StorageMonitorStatus::kError;
  } else if (current_storage < kStorageCriticallyLowThreshold) {
    status = StorageMonitorStatus::kCriticallyLow;
  } else if (current_storage < kStorageLowThreshold) {
    status = StorageMonitorStatus::kLow;
  }
  return status;
}

void ChromeCameraAppUIDelegate::StorageMonitor::MonitorCurrentStatus() {
  if (callback_.is_null()) {
    return;
  }

  auto current_status = GetCurrentStatus();

  if (status_ != current_status) {
    callback_.Run(current_status);
    status_ = current_status;
  }
}

ChromeCameraAppUIDelegate::PdfServiceManager::PdfServiceManager(
    scoped_refptr<screen_ai::OpticalCharacterRecognizer>
        optical_character_recognizer)
    : optical_character_recognizer_(optical_character_recognizer) {
  pdf_thumbnailers_.set_disconnect_handler(
      base::BindRepeating(&ChromeCameraAppUIDelegate::PdfServiceManager::
                              ConsumeGotThumbnailCallback,
                          weak_factory_.GetWeakPtr(), std::vector<uint8_t>()));
}

ChromeCameraAppUIDelegate::PdfServiceManager::~PdfServiceManager() = default;

void ChromeCameraAppUIDelegate::PdfServiceManager::GetThumbnail(
    const std::vector<uint8_t>& pdf,
    base::OnceCallback<void(const std::vector<uint8_t>&)> callback) {
  // TODO(b/329069826): To prevent the thumbnailer from adding a white
  // background to the result, get the actual dimensions and limit them to the
  // maximum supported dimensions (keeping the aspect ratio), rather than
  // passing the maximum supported dimensions directly.
  auto params = pdf::mojom::ThumbParams::New(
      /*size_px=*/gfx::Size(pdf::mojom::PdfThumbnailer::kMaxWidthPixels,
                            pdf::mojom::PdfThumbnailer::kMaxHeightPixels),
      /*dpi=*/gfx::Size(kPdfDpi, kPdfDpi),
      /*stretch_to_bounds=*/false, /*keep_aspect_ratio=*/true);
  auto pdf_region = base::ReadOnlySharedMemoryRegion::Create(pdf.size());
  if (!pdf_region.IsValid()) {
    LOG(ERROR) << "Failed to allocate memory for PDF";
    std::move(callback).Run({});
    return;
  }
  memcpy(pdf_region.mapping.memory(), pdf.data(), pdf.size());

  mojo::Remote<pdf::mojom::PdfService> pdf_service = LaunchPdfService();
  mojo::PendingRemote<pdf::mojom::PdfThumbnailer> pdf_thumbnailer;
  pdf_service->BindPdfThumbnailer(
      pdf_thumbnailer.InitWithNewPipeAndPassReceiver());
  mojo::RemoteSetElementId pdf_service_id =
      pdf_services_.Add(std::move(pdf_service));
  mojo::RemoteSetElementId pdf_thumbnailer_id =
      pdf_thumbnailers_.Add(std::move(pdf_thumbnailer));
  pdf_thumbnailer_callbacks[pdf_thumbnailer_id] = std::move(callback);
  pdf_thumbnailers_.Get(pdf_thumbnailer_id)
      ->GetThumbnail(
          std::move(params), std::move(pdf_region.region),
          base::BindOnce(
              &ChromeCameraAppUIDelegate::PdfServiceManager::GotThumbnail,
              weak_factory_.GetWeakPtr(), pdf_service_id, pdf_thumbnailer_id));
}

void ChromeCameraAppUIDelegate::PdfServiceManager::GotThumbnail(
    mojo::RemoteSetElementId pdf_service_id,
    mojo::RemoteSetElementId pdf_thumbnailer_id,
    const SkBitmap& bitmap) {
  std::vector<uint8_t> jpeg_data;
  if (gfx::JPEGCodec::Encode(bitmap, /*quality=*/100, &jpeg_data)) {
    ConsumeGotThumbnailCallback(std::move(jpeg_data), pdf_thumbnailer_id);
  } else {
    LOG(ERROR) << "Failed to encode bitmap to JPEG";
    ConsumeGotThumbnailCallback({}, pdf_thumbnailer_id);
  }
  pdf_thumbnailers_.Remove(pdf_thumbnailer_id);
  pdf_services_.Remove(pdf_service_id);
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ConsumeGotThumbnailCallback(
    const std::vector<uint8_t>& thumbnail,
    mojo::RemoteSetElementId id) {
  std::move(pdf_thumbnailer_callbacks[id]).Run(thumbnail);
  pdf_thumbnailer_callbacks.erase(id);
}

mojo::PendingRemote<pdf::mojom::Ocr>
ChromeCameraAppUIDelegate::PdfServiceManager::CreateOcrRemote() {
  mojo::PendingReceiver<::pdf::mojom::Ocr> receiver;
  mojo::PendingRemote<::pdf::mojom::Ocr> remote =
      receiver.InitWithNewPipeAndPassRemote();
  ocr_receivers_.Add(this, std::move(receiver));
  return remote;
}

void ChromeCameraAppUIDelegate::PdfServiceManager::PerformOcr(
    const SkBitmap& image,
    PerformOcrCallback callback) {
  if (base::FeatureList::IsEnabled(ash::features::kCameraAppPdfOcr)) {
    optical_character_recognizer_->PerformOCR(image, std::move(callback));
    return;
  }
  std::move(callback).Run(screen_ai::mojom::VisualAnnotation::New());
}

// TODO(b/339345727): Inform CCA earlier when the PDF service crashes.
std::unique_ptr<ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf>
ChromeCameraAppUIDelegate::PdfServiceManager::CreateProgressivePdf() {
  mojo::Remote<pdf::mojom::PdfService> pdf_service = LaunchPdfService();
  mojo::Remote<pdf::mojom::PdfProgressiveSearchifier> pdf_searchifier;
  pdf_service->BindPdfProgressiveSearchifier(
      pdf_searchifier.BindNewPipeAndPassReceiver(), CreateOcrRemote());
  return std::make_unique<
      ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf>(
      std::move(pdf_service), std::move(pdf_searchifier));
}

ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::ProgressivePdf(
    mojo::Remote<pdf::mojom::PdfService> pdf_service,
    mojo::Remote<pdf::mojom::PdfProgressiveSearchifier> pdf_searchifier)
    : pdf_service_(std::move(pdf_service)),
      pdf_searchifier_(std::move(pdf_searchifier)) {
  pdf_searchifier_.set_disconnect_handler(
      base::BindRepeating(&ChromeCameraAppUIDelegate::PdfServiceManager::
                              ProgressivePdf::ConsumeSaveCallback,
                          weak_factory_.GetWeakPtr(), std::vector<uint8_t>()));
}

ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::
    ~ProgressivePdf() = default;

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::AddPage(
    mojo_base::BigBuffer jpg,
    uint32_t index) {
  AddPageInternal(jpg, index);
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::
    AddPageInline(const std::vector<uint8_t>& jpg, uint32_t index) {
  AddPageInternal(jpg, index);
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::
    AddPageInternal(base::span<const uint8_t> jpg, uint32_t index) {
  if (!pdf_searchifier_) {
    LOG(ERROR) << "Failed to add new page to PDF";
    return;
  }
  std::unique_ptr<SkBitmap> bitmap =
      gfx::JPEGCodec::Decode(jpg.data(), jpg.size());
  pdf_searchifier_->AddPage(std::move(*bitmap), index);
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::DeletePage(
    uint32_t index) {
  if (!pdf_searchifier_) {
    LOG(ERROR) << "Failed to delete page from PDF";
    return;
  }
  pdf_searchifier_->DeletePage(index);
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::Save(
    SaveCallback callback) {
  SaveInline(base::BindOnce(
      [](SaveCallback callback, const std::vector<uint8_t>& pdf) {
        std::move(callback).Run(pdf);
      },
      std::move(callback)));
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::SaveInline(
    SaveInlineCallback callback) {
  if (!pdf_searchifier_) {
    LOG(ERROR) << "Failed to save PDF";
    std::move(callback).Run({});
    return;
  }
  save_callback_ = std::move(callback);
  pdf_searchifier_->Save(
      base::BindOnce(&ChromeCameraAppUIDelegate::PdfServiceManager::
                         ProgressivePdf::ConsumeSaveCallback,
                     weak_factory_.GetWeakPtr()));
}

void ChromeCameraAppUIDelegate::PdfServiceManager::ProgressivePdf::
    ConsumeSaveCallback(const std::vector<uint8_t>& searchified_pdf) {
  // `PDF service crashed. Avoid any further calls to `pdf_searchifier`.
  if (searchified_pdf.empty()) {
    LOG(ERROR) << "PDF Searchifier crashed";
    pdf_searchifier_.reset();
    pdf_service_.reset();
  }

  // `save_callback_` may have value if `pdf_searchifier` crashed on calling
  // `Save`.
  if (save_callback_.is_null()) {
    return;
  }
  std::move(save_callback_).Run(searchified_pdf);
}

ChromeCameraAppUIDelegate::ChromeCameraAppUIDelegate(content::WebUI* web_ui)
    : web_ui_(web_ui),
      session_start_time_(base::Time::Now()),
      file_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE})),
      storage_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE})) {
  file_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce([]() { return std::make_unique<FileMonitor>(); }),
      base::BindOnce(&ChromeCameraAppUIDelegate::OnFileMonitorInitialized,
                     weak_factory_.GetWeakPtr()));

  InitializeStorageMonitor();
  // TODO(b/338363415): Check the service availability before trying to use it.
  optical_character_recognizer_ = screen_ai::OpticalCharacterRecognizer::Create(
      Profile::FromWebUI(web_ui_), screen_ai::mojom::OcrClientType::kCameraApp);
  pdf_service_manager_ =
      std::make_unique<PdfServiceManager>(optical_character_recognizer_);
}

ChromeCameraAppUIDelegate::~ChromeCameraAppUIDelegate() {
  // Destroy |file_monitor_| on |file_task_runner_|.
  // TODO(wtlee): Ensure there is no lifetime issue before actually deleting it.
  weak_factory_.InvalidateWeakPtrs();
  file_task_runner_->DeleteSoon(FROM_HERE, std::move(file_monitor_));

  storage_task_runner_->DeleteSoon(FROM_HERE, std::move(storage_monitor_));

  // Try triggering the HaTS survey when leaving the app.
  MaybeTriggerSurvey();
}

void ChromeCameraAppUIDelegate::SetLaunchDirectory() {
  Profile* profile = Profile::FromWebUI(web_ui_);
  content::WebContents* web_contents = web_ui_->GetWebContents();

  auto my_files_folder_path =
      file_manager::util::GetMyFilesFolderForProfile(profile);

  auto* swa_manager = ash::SystemWebAppManager::Get(profile);
  if (!swa_manager) {
    return;
  }

  std::optional<webapps::AppId> app_id =
      swa_manager->GetAppIdForSystemApp(ash::SystemWebAppType::CAMERA);
  if (!app_id.has_value()) {
    return;
  }

  // The launch directory is passed here rather than
  // `SystemWebAppDelegate::LaunchAndNavigateSystemWebApp()` to handle the case
  // of the app being opened to handle an Android intent, i.e. when it's shown
  // as a dialog via `CameraAppDialog`.
  web_app::WebAppLaunchParams launch_params;
  launch_params.started_new_navigation = true;
  launch_params.app_id = *app_id;
  launch_params.target_url = GURL(ash::kChromeUICameraAppMainURL);
  launch_params.dir = my_files_folder_path;
  web_app::WebAppTabHelper::CreateForWebContents(web_contents);
  web_app::WebAppTabHelper::FromWebContents(web_contents)
      ->EnsureLaunchQueue()
      .Enqueue(std::move(launch_params));
}

void ChromeCameraAppUIDelegate::PopulateLoadTimeData(
    content::WebUIDataSource* source) {
  // Add strings that can be pulled in.
  //
  // Please also update the mocked value in _handle_strings_m_js in
  // ash/webui/camera_app_ui/resources/utils/cca/commands/dev.py when adding or
  // removing keys here.
  source->AddString("board_name", base::SysInfo::GetLsbReleaseBoard());
  source->AddString("device_type",
                    DeviceTypeToString(chromeos::GetDeviceType()));
  source->AddBoolean("digital_zoom", base::FeatureList::IsEnabled(
                                         ash::features::kCameraAppDigitalZoom));
  source->AddBoolean("preview_ocr", base::FeatureList::IsEnabled(
                                        ash::features::kCameraAppPreviewOcr));
  source->AddBoolean("super_res", base::FeatureList::IsEnabled(
                                      ash::features::kCameraSuperResSupported));

  const PrefService* prefs = Profile::FromWebUI(web_ui_)->GetPrefs();
  GURL cca_url = GURL(ash::kChromeUICameraAppURL);
  bool url_allowed = policy::IsOriginInAllowlist(
      cca_url, prefs, prefs::kVideoCaptureAllowedUrls);
  source->AddBoolean(
      "cca_disallowed",
      !prefs->GetBoolean(prefs::kVideoCaptureAllowed) && !url_allowed);

  const char kChromeOSReleaseTrack[] = "CHROMEOS_RELEASE_TRACK";
  const char kTestImageRelease[] = "testimage-channel";

  std::string track;
  bool is_test_image =
      base::SysInfo::GetLsbReleaseValue(kChromeOSReleaseTrack, &track) &&
      track.find(kTestImageRelease) != std::string::npos;
  source->AddBoolean("is_test_image", is_test_image);

  source->AddString("browser_version",
                    std::string(version_info::GetVersionNumber()));
  source->AddString("os_version", base::SysInfo::OperatingSystemVersion());

  // BigBuffer doesn't work well on ARM devices. See b/360028048.
  std::string arch = base::SysInfo::ProcessCPUArchitecture();
  source->AddBoolean("can_use_big_buffer", !base::StartsWith(arch, "ARM"));
}

bool ChromeCameraAppUIDelegate::IsMetricsAndCrashReportingEnabled() {
  // It is exposed for recording Google Analytics metrics.
  // TODO(crbug.com/1113567): Remove the method once the metrics is migrated to
  // UMA.
  return ChromeMetricsServiceAccessor::IsMetricsAndCrashReportingEnabled();
}

void ChromeCameraAppUIDelegate::OpenFileInGallery(const std::string& name) {
  base::FilePath path = GetFilePathByName(name);
  if (path.empty()) {
    return;
  }

  ash::SystemAppLaunchParams params;
  params.launch_paths = {path};
  params.launch_source = apps::LaunchSource::kFromOtherApp;
  ash::LaunchSystemWebAppAsync(Profile::FromWebUI(web_ui_),
                               ash::SystemWebAppType::MEDIA, params);
}

void ChromeCameraAppUIDelegate::OpenFeedbackDialog(
    const std::string& placeholder) {
  // TODO(crbug/1045222): Additional strings are blank right now while we decide
  // on the language and relevant information we want feedback to include.
  // Note that category_tag is the name of the listnr bucket we want our
  // reports to end up in.
  Profile* profile = Profile::FromWebUI(web_ui_);
  chrome::ShowFeedbackPage(GURL(ash::kChromeUICameraAppURL), profile,
                           feedback::kFeedbackSourceCameraApp,
                           std::string() /* description_template */,
                           placeholder /* description_placeholder_text */,
                           "chromeos-camera-app" /* category_tag */,
                           std::string() /* extra_diagnostics */);
}

std::string ChromeCameraAppUIDelegate::GetFilePathInArcByName(
    const std::string& name) {
  base::FilePath path = GetFilePathByName(name);
  if (path.empty()) {
    return std::string();
  }

  GURL arc_url_out;
  bool requires_sharing = false;
  if (!file_manager::util::ConvertPathToArcUrl(path, &arc_url_out,
                                               &requires_sharing) ||
      !arc_url_out.is_valid()) {
    return std::string();
  }
  if (requires_sharing) {
    LOG(ERROR) << "File path should be in MyFiles and not require any sharing";
    NOTREACHED_IN_MIGRATION();
    return std::string();
  }
  return arc_url_out.spec();
}

void ChromeCameraAppUIDelegate::OpenDevToolsWindow(
    content::WebContents* web_contents) {
  DevToolsWindow::OpenDevToolsWindow(web_contents, DevToolsToggleAction::NoOp(),
                                     DevToolsOpenedByAction::kUnknown);
}

void ChromeCameraAppUIDelegate::MonitorFileDeletion(
    const std::string& name,
    base::OnceCallback<void(FileMonitorResult)> callback) {
  auto file_path = GetFilePathByName(name);
  if (file_path.empty()) {
    LOG(ERROR) << "Unexpected file name: " << name;
    std::move(callback).Run(FileMonitorResult::kError);
    return;
  }
  if (!file_monitor_) {
    std::move(callback).Run(FileMonitorResult::kError);
    return;
  }

  // We should return the response on current thread (mojo thread).
  auto callback_on_current_thread =
      base::BindPostTaskToCurrentDefault(std::move(callback), FROM_HERE);
  file_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &ChromeCameraAppUIDelegate::MonitorFileDeletionOnFileThread,
          weak_factory_.GetWeakPtr(), file_monitor_.get(), std::move(file_path),
          std::move(callback_on_current_thread)));
}

void ChromeCameraAppUIDelegate::MaybeTriggerSurvey() {
  static constexpr base::TimeDelta kMinSurveyDuration = base::Seconds(15);

  if (base::Time::Now() - session_start_time_ < kMinSurveyDuration) {
    return;
  }
  CameraAppSurveyHandler::GetInstance()->MaybeTriggerSurvey();
}

void ChromeCameraAppUIDelegate::StartStorageMonitor(
    base::RepeatingCallback<void(StorageMonitorStatus)> monitor_callback) {
  if (!storage_monitor_) {
    LOG(ERROR) << "Failed to start monitoring storage due to missing monitor "
                  "instance.";
    monitor_callback.Run(StorageMonitorStatus::kError);
    return;
  }

  auto monitor_callback_on_current_thread =
      base::BindPostTaskToCurrentDefault(monitor_callback, FROM_HERE);
  auto monitor_path = GetMyFilesFolder();
  storage_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &ChromeCameraAppUIDelegate::StorageMonitor::StartMonitoring,
          storage_monitor_weak_ptr_, std::move(monitor_path),
          monitor_callback_on_current_thread));
}

void ChromeCameraAppUIDelegate::StopStorageMonitor() {
  storage_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&ChromeCameraAppUIDelegate::StorageMonitor::StopMonitoring,
                     storage_monitor_weak_ptr_));
}

void ChromeCameraAppUIDelegate::OpenStorageManagement() {
  chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
      Profile::FromWebUI(web_ui_),
      chromeos::settings::mojom::kStorageSubpagePath);
}

base::FilePath ChromeCameraAppUIDelegate::GetMyFilesFolder() {
  Profile* profile = Profile::FromWebUI(web_ui_);
  return file_manager::util::GetMyFilesFolderForProfile(profile);
}

base::FilePath ChromeCameraAppUIDelegate::GetFilePathByName(
    const std::string& name) {
  // Check to avoid directory traversal attack.
  base::FilePath name_component(name);
  if (name_component.ReferencesParent()) {
    return base::FilePath();
  }

  return GetMyFilesFolder().Append("Camera").Append(name_component);
}

void ChromeCameraAppUIDelegate::OnFileMonitorInitialized(
    std::unique_ptr<FileMonitor> file_monitor) {
  file_monitor_ = std::move(file_monitor);
}

void ChromeCameraAppUIDelegate::MonitorFileDeletionOnFileThread(
    FileMonitor* file_monitor,
    const base::FilePath& file_path,
    base::OnceCallback<void(FileMonitorResult)> callback) {
  DCHECK(file_task_runner_->RunsTasksInCurrentSequence());

  file_monitor->Monitor(file_path, std::move(callback));
}

void ChromeCameraAppUIDelegate::InitializeStorageMonitor() {
  storage_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          [](scoped_refptr<base::SequencedTaskRunner> task_runner) {
            return std::make_unique<StorageMonitor>(task_runner);
          },
          storage_task_runner_),
      base::BindOnce(&ChromeCameraAppUIDelegate::OnStorageMonitorInitialized,
                     weak_factory_.GetWeakPtr()));
}

void ChromeCameraAppUIDelegate::OnStorageMonitorInitialized(
    std::unique_ptr<StorageMonitor> monitor) {
  storage_monitor_ = std::move(monitor);
  // It's safe to get weak_ptr here as it will be used only with
  // |storage_task_runner_|, so it will be dereferenced and invalidated on the
  // same sequence.
  storage_monitor_weak_ptr_ = storage_monitor_.get()->GetWeakPtr();
}

media_device_salt::MediaDeviceSaltService*
ChromeCameraAppUIDelegate::GetMediaDeviceSaltService(
    content::BrowserContext* context) {
  return MediaDeviceSaltServiceFactory::GetInstance()->GetForBrowserContext(
      context);
}

void ChromeCameraAppUIDelegate::OpenWifiDialog(WifiConfig wifi_config) {
  auto config = chromeos::network_config::mojom::WiFiConfigProperties::New();
  config->ssid = wifi_config.ssid;
  if (wifi_config.security.empty()) {
    config->security = SecurityType::kNone;
  } else if (wifi_config.security == onc::wifi::kWPA_PSK) {
    config->security = SecurityType::kWpaPsk;
  } else if (wifi_config.security == onc::wifi::kWEP_PSK) {
    config->security = SecurityType::kWepPsk;
  } else if (wifi_config.security == onc::wifi::kWPA_EAP) {
    config->security = SecurityType::kWpaEap;
  } else {
    NOTREACHED_IN_MIGRATION()
        << "Unexpected network security type: " << wifi_config.security;
  }
  config->passphrase = wifi_config.password;
  if (config->security == SecurityType::kWpaEap) {
    auto eap_config =
        chromeos::network_config::mojom::EAPConfigProperties::New();
    eap_config->outer = wifi_config.eap_method;
    eap_config->inner = wifi_config.eap_phase2_method;
    eap_config->identity = wifi_config.eap_identity;
    eap_config->anonymous_identity = wifi_config.eap_anonymous_identity;
    eap_config->password = wifi_config.password;
    config->eap = std::move(eap_config);
  }
  ash::InternetConfigDialog::ShowDialogForNetworkWithWifiConfig(
      std::move(config));
}

std::string ChromeCameraAppUIDelegate::GetSystemLanguage() {
  auto* profile = Profile::FromWebUI(web_ui_);
  auto* pref_service = profile->GetPrefs();
  std::string accept_languages =
      pref_service->GetString(language::prefs::kAcceptLanguages);
  // Languages are splitted by ','. We only need to return the first one.
  return accept_languages.substr(0, accept_languages.find(','));
}

void ChromeCameraAppUIDelegate::RenderPdfAsJpeg(
    const std::vector<uint8_t>& pdf,
    base::OnceCallback<void(const std::vector<uint8_t>&)> callback) {
  pdf_service_manager_->GetThumbnail(pdf, std::move(callback));
}

void ChromeCameraAppUIDelegate::PerformOcr(
    base::span<const uint8_t> jpeg_data,
    base::OnceCallback<void(ash::camera_app::mojom::OcrResultPtr)> callback) {
  std::unique_ptr<SkBitmap> bitmap =
      gfx::JPEGCodec::Decode(jpeg_data.data(), jpeg_data.size());
  optical_character_recognizer_->PerformOCR(
      std::move(*bitmap),
      base::BindOnce(
          [](base::OnceCallback<void(ash::camera_app::mojom::OcrResultPtr)>
                 callback,
             screen_ai::mojom::VisualAnnotationPtr annotation) {
            auto result = ash::camera_app::mojom::OcrResult::New();
            for (const auto& line_box : annotation->lines) {
              auto line = ash::camera_app::mojom::Line::New();
              line->text = line_box->text_line;
              line->bounding_box = std::move(line_box->bounding_box);
              line->bounding_box_angle = line_box->bounding_box_angle;
              line->language = line_box->language;
              line->confidence = line_box->confidence;
              for (const auto& word_box : line_box->words) {
                auto word = ash::camera_app::mojom::Word::New();
                word->bounding_box = std::move(word_box->bounding_box);
                word->bounding_box_angle = word_box->bounding_box_angle;
                word->text = word_box->word;
                if (word_box->direction ==
                    screen_ai::mojom::Direction::DIRECTION_RIGHT_TO_LEFT) {
                  word->direction =
                      ash::camera_app::mojom::WordDirection::kRightToLeft;
                } else {
                  word->direction =
                      ash::camera_app::mojom::WordDirection::kLeftToRight;
                }
                line->words.push_back(std::move(word));
              }
              result->lines.push_back(std::move(line));
            }
            std::move(callback).Run(std::move(result));
          },
          std::move(callback)));
}

void ChromeCameraAppUIDelegate::CreatePdfBuilder(
    mojo::PendingReceiver<ash::camera_app::mojom::PdfBuilder> receiver) {
  mojo::MakeSelfOwnedReceiver(pdf_service_manager_->CreateProgressivePdf(),
                              std::move(receiver));
}

ash::CameraAppUIDelegate::WifiConfig::WifiConfig() = default;

ash::CameraAppUIDelegate::WifiConfig::WifiConfig(
    const ash::CameraAppUIDelegate::WifiConfig&) = default;

ash::CameraAppUIDelegate::WifiConfig&
ash::CameraAppUIDelegate::WifiConfig::operator=(
    const ash::CameraAppUIDelegate::WifiConfig&) = default;

ash::CameraAppUIDelegate::WifiConfig::~WifiConfig() = default;