chromium/ash/system/camera/camera_effects_controller.cc

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

#include "ash/system/camera/camera_effects_controller.h"

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/wallpaper/sea_pen_image.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/camera/autozoom_controller_impl.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "ash/system/video_conference/video_conference_tray.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_utils.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "media/capture/video/chromeos/camera_hal_dispatcher_impl.h"
#include "media/capture/video/chromeos/mojom/cros_camera_service.mojom-shared.h"
#include "media/capture/video/chromeos/video_capture_features_chromeos.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image_util.h"
#include "ui/gfx/vector_icon_types.h"

namespace ash {

namespace {

// A `std::pair` representation of the background blur state that
// `CameraHalDispatcherImpl` expects:
// - `BlurLevel` that specifies how much blur to apply
// - `bool` that's 'true' if background blur is enabled, false otherwise
using CameraHalBackgroundBlurState = std::pair<cros::mojom::BlurLevel, bool>;

using BackgroundImageInfo = CameraEffectsController::BackgroundImageInfo;

// Directory used for saving camera backgrounds.
constexpr char kCameraBackgroundOriginalDir[] =
    "custom-camera-backgrounds/original";

constexpr char kMetadataSuffix[] = ".metadata";

constexpr char kSupportedImages[] = FILE_PATH_LITERAL("*.jpg");

constexpr unsigned int k3M = 3 * 1024 * 1024;

// Max number of images kept as camera background.
constexpr unsigned int kMaxNumberOfImageKeptOnDisk = 12;

// Directory that can be accessed by the camera module.
constexpr char kImageDirForCameraModule[] = "/run/camera/";

// Returns 'true' if `pref_value` is an allowable value of
// `CameraEffectsController::BackgroundBlurPrefValue`, 'false' otherwise.
bool IsValidBackgroundBlurPrefValue(int pref_value) {
  switch (pref_value) {
    case CameraEffectsController::BackgroundBlurPrefValue::kOff:
    case CameraEffectsController::BackgroundBlurPrefValue::kLowest:
    case CameraEffectsController::BackgroundBlurPrefValue::kLight:
    case CameraEffectsController::BackgroundBlurPrefValue::kMedium:
    case CameraEffectsController::BackgroundBlurPrefValue::kHeavy:
    case CameraEffectsController::BackgroundBlurPrefValue::kMaximum:
    case CameraEffectsController::BackgroundBlurPrefValue::kImage:
      return true;
  }

  return false;
}

// Maps `pref_value` (assumed to be a value read out of
// `prefs::kBackgroundBlur`) to a `CameraHalBackgroundBlurState` (that
// `CameraHalDispatcherImpl` expects).
CameraHalBackgroundBlurState MapBackgroundBlurPrefValueToCameraHalState(
    int pref_value) {
  DCHECK(IsValidBackgroundBlurPrefValue(pref_value));

  switch (pref_value) {
    // For state `kOff`, the `bool` is 'false' because background blur is
    // disabled, `BlurLevel` is set to `kLowest` but its value doesn't matter.
    case CameraEffectsController::BackgroundBlurPrefValue::kOff:
    case CameraEffectsController::BackgroundBlurPrefValue::kImage:
      return std::make_pair(cros::mojom::BlurLevel::kLowest, false);

    // For states other than `kOff`, background blur is enabled so the `bool`
    // is set to 'true' and `pref_value` is mapped to a `BlurLevel`.
    case CameraEffectsController::BackgroundBlurPrefValue::kLowest:
      return std::make_pair(cros::mojom::BlurLevel::kLowest, true);
    case CameraEffectsController::BackgroundBlurPrefValue::kLight:
      return std::make_pair(cros::mojom::BlurLevel::kLight, true);
    case CameraEffectsController::BackgroundBlurPrefValue::kMedium:
      return std::make_pair(cros::mojom::BlurLevel::kMedium, true);
    case CameraEffectsController::BackgroundBlurPrefValue::kHeavy:
      return std::make_pair(cros::mojom::BlurLevel::kHeavy, true);
    case CameraEffectsController::BackgroundBlurPrefValue::kMaximum:
      return std::make_pair(cros::mojom::BlurLevel::kMaximum, true);
  }

  NOTREACHED();
}

// Maps the `CameraHalDispatcherImpl`-ready background blur state
// `level`/`enabled` to `CameraEffectsController::BackgroundBlurPrefValue`,
// which is what's written to `prefs::kBackgroundBlur`.
CameraEffectsController::BackgroundBlurPrefValue
MapBackgroundBlurCameraHalStateToPrefValue(cros::mojom::BlurLevel level,
                                           bool enabled) {
  if (!enabled) {
    return CameraEffectsController::BackgroundBlurPrefValue::kOff;
  }

  switch (level) {
    case cros::mojom::BlurLevel::kLowest:
      return CameraEffectsController::BackgroundBlurPrefValue::kLowest;
    case cros::mojom::BlurLevel::kLight:
      return CameraEffectsController::BackgroundBlurPrefValue::kLight;
    case cros::mojom::BlurLevel::kMedium:
      return CameraEffectsController::BackgroundBlurPrefValue::kMedium;
    case cros::mojom::BlurLevel::kHeavy:
      return CameraEffectsController::BackgroundBlurPrefValue::kHeavy;
    case cros::mojom::BlurLevel::kMaximum:
      return CameraEffectsController::BackgroundBlurPrefValue::kMaximum;
  }

  NOTREACHED();
}

CameraEffectsController::BackgroundBlurState MapBackgroundBlurPrefValueToState(
    int pref_value) {
  DCHECK(IsValidBackgroundBlurPrefValue(pref_value));

  switch (pref_value) {
    case CameraEffectsController::BackgroundBlurPrefValue::kOff:
      return CameraEffectsController::BackgroundBlurState::kOff;
    case CameraEffectsController::BackgroundBlurPrefValue::kLowest:
      return CameraEffectsController::BackgroundBlurState::kLowest;
    case CameraEffectsController::BackgroundBlurPrefValue::kLight:
      return CameraEffectsController::BackgroundBlurState::kLight;
    case CameraEffectsController::BackgroundBlurPrefValue::kMedium:
      return CameraEffectsController::BackgroundBlurState::kMedium;
    case CameraEffectsController::BackgroundBlurPrefValue::kHeavy:
      return CameraEffectsController::BackgroundBlurState::kHeavy;
    case CameraEffectsController::BackgroundBlurPrefValue::kMaximum:
      return CameraEffectsController::BackgroundBlurState::kMaximum;
    case CameraEffectsController::BackgroundBlurPrefValue::kImage:
      return CameraEffectsController::BackgroundBlurState::kImage;
  }

  NOTREACHED();
}

inline base::FilePath GetMetadataFilePath(const base::FilePath& filepath) {
  return filepath.AddExtensionASCII(kMetadataSuffix);
}

// Remove the file and its metadata if exists.
bool RemoveBackgroundImageOnWorker(const base::FilePath& filepath) {
  if (!base::DeleteFile(filepath)) {
    return false;
  }

  const auto metadata_filepath = GetMetadataFilePath(filepath);
  if (base::PathExists(metadata_filepath) &&
      !base::DeleteFile(metadata_filepath)) {
    return false;
  }

  return true;
}

// Writes `jpeg_bytes` to the `camera_background_img_dir`.
// Returns basename if succeeds, empty path otherwise.
base::FilePath WriteImageToBackgroundDir(
    const base::FilePath& camera_background_img_dir,
    SeaPenImage&& sea_pen_image,
    const std::string& metadata) {
  const base::FilePath basename =
      CameraEffectsController::SeaPenIdToRelativePath(sea_pen_image.id);
  const base::FilePath background_image_filepath =
      camera_background_img_dir.Append(basename);
  const base::FilePath background_metadata_filepath =
      GetMetadataFilePath(background_image_filepath);

  if (base::CreateDirectory(camera_background_img_dir) &&
      base::WriteFile(background_image_filepath, sea_pen_image.jpg_bytes) &&
      base::WriteFile(background_metadata_filepath, metadata)) {
    return basename;
  }

  // We don't want keep corrupted images.
  RemoveBackgroundImageOnWorker(background_image_filepath);
  return base::FilePath();
}

// Copies image file from `background_image_filepath` to
// `background_run_filepath`.
bool CopyBackgroundImageFile(const base::FilePath& background_image_filepath,
                             const base::FilePath& background_run_filepath) {
  const base::FilePath background_run_dir = background_run_filepath.DirName();
  const base::FilePath basename = background_run_filepath.BaseName();

  if (base::CreateDirectory(background_run_dir) &&
      base::CopyFile(background_image_filepath, background_run_filepath)) {
    base::File::Info file_info;
    base::GetFileInfo(background_image_filepath, &file_info);
    base::TouchFile(background_image_filepath, base::Time::Now(),
                    file_info.last_modified);

    // Remove all other images in the background_run_dir`.
    base::FileEnumerator enumerator(
        background_run_dir,
        /*recursive=*/false, base::FileEnumerator::FILES, kSupportedImages);
    for (auto path = enumerator.Next(); !path.empty();
         path = enumerator.Next()) {
      if (enumerator.GetInfo().GetName() != basename) {
        base::DeleteFile(path);
      }
    }

    return true;
  }
  LOG(ERROR) << "Can't copy " << background_image_filepath << " to "
             << background_run_filepath;

  return false;
}

// Returns a full list of files inside `camera_background_img_dir`, sorted by
// last_accessed time.
std::vector<base::FilePath> GetBackgroundImageFileNamesOnWorker(
    const base::FilePath& camera_background_img_dir) {
  using FileNameAndTime = std::pair<base::FilePath, base::Time>;

  std::vector<FileNameAndTime> filenames_and_modified_time;

  // Loop through all files in `camera_background_img_dir`.
  base::FileEnumerator enumerator(
      camera_background_img_dir,
      /*recursive=*/false, base::FileEnumerator::FILES, kSupportedImages);
  for (auto path = enumerator.Next(); !path.empty(); path = enumerator.Next()) {
    base::File::Info file_info;
    base::GetFileInfo(path, &file_info);
    filenames_and_modified_time.push_back(
        {path.BaseName(), file_info.last_accessed});
  }

  // Sorted by last_accessed.
  std::sort(filenames_and_modified_time.begin(),
            filenames_and_modified_time.end(),
            [](const FileNameAndTime& f1, const FileNameAndTime& f2) {
              return f1.second > f2.second;
            });

  // Only keep the latest `kMaxNumberOfImageKeptOnDisk` images on disk.
  if (filenames_and_modified_time.size() > kMaxNumberOfImageKeptOnDisk) {
    for (std::size_t i = kMaxNumberOfImageKeptOnDisk;
         i < filenames_and_modified_time.size(); i++) {
      const auto filename = camera_background_img_dir.Append(
          filenames_and_modified_time[i].first);
      RemoveBackgroundImageOnWorker(filename);
    }

    filenames_and_modified_time.resize(kMaxNumberOfImageKeptOnDisk);
  }

  std::vector<base::FilePath> filenames;
  for (auto& filename_and_time : filenames_and_modified_time) {
    filenames.push_back(filename_and_time.first);
  }

  return filenames;
}

// Gets the BackgroundImageInfo of the `filename`.
std::optional<BackgroundImageInfo> GetBackgroundImageInfoOnWorker(
    const base::FilePath& filename) {
  base::File::Info file_info;
  if (!base::GetFileInfo(filename, &file_info)) {
    return std::nullopt;
  }

  BackgroundImageInfo info{file_info.creation_time, file_info.last_accessed,
                           filename.BaseName(), gfx::ImageSkia(), ""};

  const std::optional<std::vector<uint8_t>> jpeg_bytes =
      base::ReadFileToBytes(filename);
  if (!jpeg_bytes) {
    return std::nullopt;
  }

  auto image = gfx::ImageFrom1xJPEGEncodedData(&jpeg_bytes.value()[0],
                                               jpeg_bytes.value().size());
  if (image.IsEmpty()) {
    return std::nullopt;
  }

  if (image.Width() > CameraEffectsController::kImageAsIconWidth) {
    const auto new_size = gfx::ScaleToCeiledSize(
        image.Size(),
        static_cast<float>(CameraEffectsController::kImageAsIconWidth) /
            image.Width());
    image = gfx::ResizedImage(image, new_size);
  }
  info.image = image.AsImageSkia();

  // if the metadata is not read successfully, then set it as empty.
  if (!base::ReadFileToString(GetMetadataFilePath(filename), &info.metadata)) {
    info.metadata = "";
  }

  return info;
}

// Reads from the `camera_background_img_dir` for the BackgroundImageInfo of the
// latest `number_of_images`.
std::vector<BackgroundImageInfo> GetRecentlyUsedBackgroundImagesOnWorker(
    const std::size_t number_of_images,
    const base::FilePath& camera_background_img_dir) {
  std::vector<base::FilePath> basenames =
      GetBackgroundImageFileNamesOnWorker(camera_background_img_dir);

  std::vector<BackgroundImageInfo> background_image_info;

  // Adds creation_time and jpeg_bytes for each image file.
  for (auto& basename : basenames) {
    const auto info = GetBackgroundImageInfoOnWorker(
        camera_background_img_dir.Append(basename));

    if (!info.has_value()) {
      continue;
    }

    background_image_info.push_back(info.value());

    if (background_image_info.size() == number_of_images) {
      break;
    }
  }

  return background_image_info;
}

void SetBackgroundReplaceUiVisible(bool visible) {
  for (auto* root_window_controller :
       Shell::Get()->GetAllRootWindowControllers()) {
    DCHECK(root_window_controller);
    DCHECK(root_window_controller->GetStatusAreaWidget());

    root_window_controller->GetStatusAreaWidget()
        ->video_conference_tray()
        ->SetBackgroundReplaceUiVisible(visible);
  }
}

cros::mojom::InferenceBackend GetInferenceBackend(
    const base::Feature& feature) {
  const std::string value =
      GetFieldTrialParamValueByFeature(feature, "inference_backend");
  if (value == "AUTO") {
    return cros::mojom::InferenceBackend::kAuto;
  } else if (value == "GPU") {
    return cros::mojom::InferenceBackend::kGpu;
  } else if (value == "NPU") {
    return cros::mojom::InferenceBackend::kNpu;
  } else {
    // If the feature is disabled, or enabled without a specific value, we will
    // get an empty string and fall into this case.
    return cros::mojom::InferenceBackend::kDefaultValue;
  }
}

}  // namespace

// static
void CameraEffectsController::RegisterProfilePrefs(
    PrefRegistrySimple* registry) {
  if (!features::IsVideoConferenceEnabled()) {
    return;
  }

  // We have to register all camera effects prefs; because we need use them to
  // construct the cros::mojom::EffectsConfigPtr.
  registry->RegisterIntegerPref(prefs::kBackgroundBlur,
                                BackgroundBlurPrefValue::kOff);

  registry->RegisterBooleanPref(prefs::kBackgroundReplace, false);

  // If the Studio Look feature is available, the portrait relighting and face
  // retouch prefs are used to determine which effects are applied. Enable both
  // of them by default. Otherwise, disable both of them.
  registry->RegisterBooleanPref(prefs::kPortraitRelighting,
                                features::IsVcStudioLookEnabled());
  registry->RegisterBooleanPref(prefs::kFaceRetouch,
                                features::IsVcStudioLookEnabled());

  // If the Studio Look feature is available, disable Studio Look by default.
  // Otherwise, set it to always true to apply effects based on the portrait
  // relighting and face retouch pref values.
  registry->RegisterBooleanPref(prefs::kStudioLook,
                                !features::IsVcStudioLookEnabled());

  registry->RegisterFilePathPref(prefs::kBackgroundImagePath, base::FilePath());
}

// static
base::FilePath CameraEffectsController::SeaPenIdToRelativePath(uint32_t id) {
  return base::FilePath(base::NumberToString(id)).AddExtension(".jpg");
}

BackgroundImageInfo::BackgroundImageInfo(const BackgroundImageInfo& info) =
    default;
BackgroundImageInfo::BackgroundImageInfo(const base::Time& creation_time,
                                         const base::Time& last_accessed,
                                         const base::FilePath& basename,
                                         const gfx::ImageSkia& image,
                                         const std::string& metadata)
    : creation_time(creation_time),
      last_accessed(last_accessed),
      basename(basename),
      image(image),
      metadata(metadata) {}

CameraEffectsController::CameraEffectsController()
    : camera_background_run_dir_(kImageDirForCameraModule),
      main_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()),
      blocking_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
  auto* session_controller = Shell::Get()->session_controller();
  DCHECK(session_controller);
  session_observation_.Observe(session_controller);

  current_effects_ = cros::mojom::EffectsConfig::New();

  // The effects are not applied when this is constructed, observe for changes
  // that will come later.
  scoped_camera_effect_observation_.Observe(
      media::CameraHalDispatcherImpl::GetInstance());

  Shell::Get()->autozoom_controller()->AddObserver(this);
}

CameraEffectsController::~CameraEffectsController() {
  VideoConferenceTrayEffectsManager& effects_manager =
      VideoConferenceTrayController::Get()->GetEffectsManager();
  if (effects_manager.IsDelegateRegistered(this)) {
    // The `VcEffectsDelegate` was registered, so must therefore be
    // unregistered.
    effects_manager.UnregisterDelegate(this);
  }

  Shell::Get()->autozoom_controller()->RemoveObserver(this);
}

cros::mojom::EffectsConfigPtr CameraEffectsController::GetCameraEffects() {
  return current_effects_.Clone();
}

void CameraEffectsController::SetBackgroundImage(
    const base::FilePath& relative_path,
    base::OnceCallback<void(bool)> callback) {
  CHECK(!camera_background_img_dir_.empty())
      << "SetBackgroundImage should not be called when "
         "camera_background_img_dir_ is not set.";

  cros::mojom::EffectsConfigPtr new_effects = current_effects_.Clone();

  if (new_effects->replace_enabled &&
      new_effects->background_filepath == relative_path) {
    std::move(callback).Run(true);
    return;
  }

  new_effects->replace_enabled = true;
  new_effects->background_filepath = relative_path;

  SetCameraEffects(std::move(new_effects), /*is_initialization*/ false,
                   std::move(callback));
}

void CameraEffectsController::SetBackgroundImageFromContent(
    const SeaPenImage& sea_pen_image,
    const std::string& metadata,
    base::OnceCallback<void(bool)> callback) {
  CHECK(!camera_background_img_dir_.empty())
      << "SetBackgroundImageFromContent should not be called when "
         "camera_background_img_dir_ is not set.";

  CHECK(!sea_pen_image.jpg_bytes.empty());
  CHECK_LT(sea_pen_image.jpg_bytes.size(), k3M)
      << "Can't use an image that is larger than 30M as a background";

  // Write images to disk;
  // TODO(b/321122378) remove unnecessary copy of SeaPenImage.
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&WriteImageToBackgroundDir, camera_background_img_dir_,
                     SeaPenImage(sea_pen_image.jpg_bytes, sea_pen_image.id),
                     metadata),
      base::BindOnce(
          &CameraEffectsController::OnSaveBackgroundImageFileComplete,
          weak_factory_.GetWeakPtr(), std::move(callback)));
}

void CameraEffectsController::RemoveBackgroundImage(
    const base::FilePath& basename,
    base::OnceCallback<void(bool)> callback) {
  CHECK(!camera_background_img_dir_.empty())
      << "RemoveBackgroundImage should not be called when "
         "camera_background_img_dir_ is not set.";

  // If the file to remove is current camera background, then reset the camera
  // background effects.
  if (basename == current_effects_->background_filepath) {
    cros::mojom::EffectsConfigPtr new_effects = GetCameraEffects();
    new_effects->replace_enabled = false;
    new_effects->background_filepath.reset();

    SetCameraEffects(std::move(new_effects), /*is_initialization*/ false,
                     base::NullCallback());
  }

  // Remove file.
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&RemoveBackgroundImageOnWorker,
                     camera_background_img_dir_.Append(basename)),
      std::move(callback));
}

void CameraEffectsController::GetRecentlyUsedBackgroundImages(
    const int number_of_images,
    base::OnceCallback<void(const std::vector<BackgroundImageInfo>&)>
        callback) {
  CHECK(!camera_background_img_dir_.empty())
      << "GetRecentlyUsedBackgroundImages should not be called when "
         "camera_background_img_dir_ is not set.";

  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetRecentlyUsedBackgroundImagesOnWorker, number_of_images,
                     camera_background_img_dir_),
      std::move(callback));
}

void CameraEffectsController::GetBackgroundImageFileNames(
    base::OnceCallback<void(const std::vector<base::FilePath>&)> callback) {
  CHECK(!camera_background_img_dir_.empty())
      << "GetBackgroundImageFileNames should not be called when "
         "camera_background_img_dir_ is not set.";

  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetBackgroundImageFileNamesOnWorker,
                     camera_background_img_dir_),
      std::move(callback));
}

void CameraEffectsController::GetBackgroundImageInfo(
    const base::FilePath& basename,
    base::OnceCallback<void(const std::optional<BackgroundImageInfo>&)>
        callback) {
  CHECK(!camera_background_img_dir_.empty())
      << "GetRecentlyUsedBackgroundImages should not be called when "
         "camera_background_img_dir_ is not set.";

  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetBackgroundImageInfoOnWorker,
                     camera_background_img_dir_.Append(basename)),
      std::move(callback));
}

// Set the `camera_background_img_dir_` when the `account_id` becomes active.
void CameraEffectsController::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  is_eligible_for_background_replace_ =
      features::IsVcBackgroundReplaceEnabled() &&
      std::get<0>(
          Shell::Get()->session_controller()->IsEligibleForSeaPen(account_id));

  is_background_replace_disabled_by_enterprise_ = !std::get<1>(
      Shell::Get()->session_controller()->IsEligibleForSeaPen(account_id));

  const base::FilePath profile_path =
      Shell::Get()->session_controller()->GetProfilePath(account_id);
  CHECK(!profile_path.empty())
      << "Profile path should not be empty in OnActiveUserSessionChanged.";

  camera_background_img_dir_ =
      profile_path.Append(kCameraBackgroundOriginalDir);

  // Initialze camera effects if the `pref_change_registrar_` is set.
  // TODO(b/321585013): figure out the order of OnActiveUserSessionChanged and
  // OnActiveUserPrefServiceChanged, and only initialize in one place.
  if (pref_change_registrar_) {
    SetCameraEffects(GetEffectsConfigFromPref(), /*is_initialization*/ true,
                     base::DoNothing());
  }

  // If any effects have controls the user can access, this will create the
  // effects UI and register `CameraEffectsController`'s `VcEffectsDelegate`
  // interface.
  InitializeEffectControls();
}

void CameraEffectsController::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  if (pref_change_registrar_ &&
      pref_service == pref_change_registrar_->prefs()) {
    return;
  }

  // Initial login and user switching in multi profiles.
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(pref_service);

  // Initialze camera effects if the `camera_background_img_dir_` is set.
  if (!camera_background_img_dir_.empty()) {
    // If the camera has started, it won't get the previous setting so call it
    // here too. If the camera service isn't ready it this call will be ignored.
    SetCameraEffects(GetEffectsConfigFromPref(), /*is_initialization*/ true,
                     base::DoNothing());
  }
}

std::optional<int> CameraEffectsController::GetEffectState(
    VcEffectId effect_id) {
  switch (effect_id) {
    case VcEffectId::kBackgroundBlur:
      return current_effects_->replace_enabled
                 ? CameraEffectsController::BackgroundBlurPrefValue::kImage
                 : MapBackgroundBlurCameraHalStateToPrefValue(
                       current_effects_->blur_level,
                       current_effects_->blur_enabled);
    case VcEffectId::kPortraitRelighting:
      return current_effects_->relight_enabled;
    case VcEffectId::kFaceRetouch:
      return current_effects_->retouch_enabled;
    case VcEffectId::kStudioLook:
      return current_effects_->studio_look_enabled;
    case VcEffectId::kCameraFraming:
      return Shell::Get()->autozoom_controller()->GetState() !=
             cros::mojom::CameraAutoFramingState::OFF;
    case VcEffectId::kNoiseCancellation:
    case VcEffectId::kStyleTransfer:
    case VcEffectId::kLiveCaption:
    case VcEffectId::kTestEffect:
      NOTREACHED();
  }
}

void CameraEffectsController::OnEffectControlActivated(
    VcEffectId effect_id,
    std::optional<int> state) {
  cros::mojom::EffectsConfigPtr new_effects = current_effects_.Clone();

  switch (effect_id) {
    case VcEffectId::kBackgroundBlur: {
      // UI should not pass in any invalid state.
      if (!state.has_value() ||
          !IsValidBackgroundBlurPrefValue(state.value())) {
        state = static_cast<int>(
            CameraEffectsController::BackgroundBlurPrefValue::kOff);
      }
      if (state.value() ==
          CameraEffectsController::BackgroundBlurPrefValue::kImage) {
        SetBackgroundReplaceUiVisible(true);

        // Clicking on the Image button should just show the
        // BackgroundReplaceUi, no effects change is required.
        return;
      }

      // Only change the SetCameraBackgroundView visibility if background
      // replace is enabled; otherwise the view is null.
      if (is_eligible_for_background_replace_) {
        SetBackgroundReplaceUiVisible(false);
      }

      auto [blur_level, blur_enabled] =
          MapBackgroundBlurPrefValueToCameraHalState(state.value());
      new_effects->blur_level = blur_level;
      new_effects->blur_enabled = blur_enabled;

      // No matter which background blur button the user clicked on, we should
      // always turn off background replace.
      new_effects->replace_enabled = false;
      new_effects->background_filepath.reset();
      break;
    }
    case VcEffectId::kPortraitRelighting: {
      new_effects->relight_enabled =
          state.value_or(!new_effects->relight_enabled);
      if (!features::IsVcStudioLookEnabled()) {
        // Make sure that `studio_look_enabled` is set to true. Otherwise, this
        // will override the value of `relight_enabled`.
        new_effects->studio_look_enabled = true;
      }
      // TODO(b/354069928): Toggle off the Studio Look button when both
      // relighting and retouch are disabled.
      break;
    }
    case VcEffectId::kFaceRetouch: {
      new_effects->retouch_enabled =
          state.value_or(!new_effects->retouch_enabled);
      if (!features::IsVcStudioLookEnabled()) {
        // Make sure that `studio_look_enabled` is set to true. Otherwise, this
        // will override the value of `retouch_enabled`.
        new_effects->studio_look_enabled = true;
      }
      // TODO(b/354069928): Toggle off the Studio Look button when both
      // relighting and retouch are disabled.
      break;
    }
    case VcEffectId::kStudioLook: {
      new_effects->studio_look_enabled =
          state.value_or(!new_effects->studio_look_enabled);
      if (new_effects->studio_look_enabled && !new_effects->relight_enabled &&
          !new_effects->retouch_enabled) {
        // When Studio Look is toggled enabled but portrait relighting and face
        // retouch are currently both disabled, the portrait relighting and face
        // retouch prefs are updated to be enabled.
        new_effects->relight_enabled = true;
        new_effects->retouch_enabled = true;
      }
      break;
    }
    case VcEffectId::kCameraFraming: {
      Shell::Get()->autozoom_controller()->Toggle();
      break;
    }
    case VcEffectId::kNoiseCancellation:
    case VcEffectId::kStyleTransfer:
    case VcEffectId::kLiveCaption:
    case VcEffectId::kTestEffect:
      NOTREACHED();
  }

  SetCameraEffects(std::move(new_effects), /*is_initialization*/ false,
                   base::DoNothing());
}

void CameraEffectsController::RecordMetricsForSetValueEffectOnClick(
    VcEffectId effect_id,
    int state_value) const {
  // `CameraEffectsController` currently only has background blur as a set-value
  // effect, so it shouldn't be any other effects here.
  DCHECK_EQ(VcEffectId::kBackgroundBlur, effect_id);

  base::UmaHistogramEnumeration(
      video_conference_utils::GetEffectHistogramNameForClick(effect_id),
      MapBackgroundBlurPrefValueToState(state_value));
}

void CameraEffectsController::RecordMetricsForSetValueEffectOnStartup(
    VcEffectId effect_id,
    int state_value) const {
  // `CameraEffectsController` currently only has background blur as a set-value
  // effect, so it shouldn't be any other effects here.
  DCHECK_EQ(VcEffectId::kBackgroundBlur, effect_id);

  base::UmaHistogramEnumeration(
      video_conference_utils::GetEffectHistogramNameForInitialState(effect_id),
      MapBackgroundBlurPrefValueToState(state_value));
}

void CameraEffectsController::OnCameraEffectChanged(
    const cros::mojom::EffectsConfigPtr& new_effects) {
  // As `CameraHalDispatcher` notifies the `new_effects` from a different
  // thread, we want to ensure the `current_effects_` is always accessed through
  // the `main_task_runner_`.
  if (!main_task_runner_->RunsTasksInCurrentSequence()) {
    main_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&CameraEffectsController::OnCameraEffectChanged,
                       weak_factory_.GetWeakPtr(), new_effects.Clone()));
    return;
  }

  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
  // If `SetCamerEffects()` finished, update `current_effects_` and prefs.
  if (!new_effects.is_null()) {
    SetEffectsConfigToPref(new_effects.Clone());
    current_effects_ = new_effects.Clone();
  }
}

void CameraEffectsController::OnAutozoomControlEnabledChanged(bool enabled) {
  if (!enabled) {
    RemoveEffect(VcEffectId::kCameraFraming);
    return;
  }

  // Using `base::Unretained()` is safe here since `this` owns the created
  // VcHostedEffect after calling `AddEffect()` below.
  std::unique_ptr<VcHostedEffect> effect = std::make_unique<VcHostedEffect>(
      /*type=*/VcEffectType::kToggle,
      /*get_state_callback=*/
      base::BindRepeating(&CameraEffectsController::GetEffectState,
                          base::Unretained(this), VcEffectId::kCameraFraming),
      /*effect_id=*/VcEffectId::kCameraFraming);

  auto effect_state = std::make_unique<VcEffectState>(
      /*icon=*/&kVideoConferenceCameraFramingOnIcon,
      /*label_text=*/
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_AUTOZOOM_BUTTON_LABEL),
      /*accessible_name_id=*/
      IDS_ASH_STATUS_TRAY_AUTOZOOM_BUTTON_LABEL,
      /*button_callback=*/
      base::BindRepeating(&CameraEffectsController::OnEffectControlActivated,
                          base::Unretained(this),
                          /*effect_id=*/VcEffectId::kCameraFraming,
                          /*value=*/std::nullopt));
  effect->AddState(std::move(effect_state));

  effect->set_dependency_flags(VcHostedEffect::ResourceDependency::kCamera);
  AddEffect(std::move(effect));
}

cros::mojom::SegmentationModel
CameraEffectsController::GetSegmentationModelType() {
  cros::mojom::SegmentationModel model_type =
      cros::mojom::SegmentationModel::kHighResolution;
  const std::string segmentation_model_param = GetFieldTrialParamValueByFeature(
      ash::features::kVcSegmentationModel, "segmentation_model");

  if (segmentation_model_param == "lower_resolution") {
    model_type = cros::mojom::SegmentationModel::kLowerResolution;
  }

  return model_type;
}

void CameraEffectsController::SetCameraEffects(
    cros::mojom::EffectsConfigPtr config,
    bool is_initialization,
    base::OnceCallback<void(bool)> copy_background_image_complete_callback) {
  // For backwards compatibility, will be removed after mojom is updated.
  if (config->blur_enabled) {
    config->effect = cros::mojom::CameraEffect::kBackgroundBlur;
  }
  if (config->replace_enabled) {
    config->effect = cros::mojom::CameraEffect::kBackgroundReplace;
  }
  if (config->relight_enabled) {
    config->effect = cros::mojom::CameraEffect::kPortraitRelight;
  }

  // Update effects config with settings from feature flags.
  config->segmentation_model = GetSegmentationModelType();
  double intensity = GetFieldTrialParamByFeatureAsDouble(
      ash::features::kVcLightIntensity, "light_intensity", -1.0);
  // Only set if its overridden by flags, otherwise use default from lib.
  if (intensity > 0.0) {
    config->light_intensity = intensity;
  }

  config->segmentation_inference_backend =
      GetInferenceBackend(ash::features::kVcSegmentationInferenceBackend);
  config->relighting_inference_backend =
      GetInferenceBackend(ash::features::kVcRelightingInferenceBackend);

  if (config->replace_enabled &&
      config->background_filepath != current_effects_->background_filepath) {
    const base::FilePath background_image_filepath =
        camera_background_img_dir_.Append(config->background_filepath.value());
    const base::FilePath background_run_filepath =
        camera_background_run_dir_.Append(config->background_filepath.value());

    // Copy image file on the worker thread first.
    blocking_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE,
        base::BindOnce(&CopyBackgroundImageFile, background_image_filepath,
                       background_run_filepath),
        base::BindOnce(
            &CameraEffectsController::OnCopyBackgroundImageFileComplete,
            weak_factory_.GetWeakPtr(), std::move(config), is_initialization,
            std::move(copy_background_image_complete_callback)));
    return;
  }

  SetCameraEffectsInCameraHalDispatcherImpl(std::move(config));
}

void CameraEffectsController::OnCopyBackgroundImageFileComplete(
    cros::mojom::EffectsConfigPtr new_config,
    bool is_initialization,
    base::OnceCallback<void(bool)> copy_background_image_complete_callback,
    bool copy_succeeded) {
  std::move(copy_background_image_complete_callback).Run(copy_succeeded);

  // If copy_succeeded, continue to apply all effects.
  if (copy_succeeded) {
    new_config->blur_enabled = false;
    SetCameraEffectsInCameraHalDispatcherImpl(std::move(new_config));
    return;
  }

  // If copy_succeeded is false, but is_initialization is true, then apply the
  // rest of the effefcts. We only want to continue when it is initialization,
  // because we don't want to randomly turn off the user's background effects
  // due to the failure of copying the new image file.
  if (is_initialization) {
    new_config->replace_enabled = false;
    new_config->background_filepath.reset();
    SetCameraEffectsInCameraHalDispatcherImpl(std::move(new_config));
  }
}

void CameraEffectsController::OnSaveBackgroundImageFileComplete(
    base::OnceCallback<void(bool)> callback,
    const base::FilePath& basename) {
  if (basename.empty()) {
    LOG(ERROR) << "Failed to write the image file: " << basename;
    std::move(callback).Run(false);
    return;
  }

  SetBackgroundImage(basename, std::move(callback));
}

cros::mojom::EffectsConfigPtr
CameraEffectsController::GetEffectsConfigFromPref() {
  cros::mojom::EffectsConfigPtr effects = cros::mojom::EffectsConfig::New();
  if (!pref_change_registrar_ || !pref_change_registrar_->prefs()) {
    return effects;
  }

  int background_blur_state_in_pref =
      pref_change_registrar_->prefs()->GetInteger(prefs::kBackgroundBlur);
  if (!IsValidBackgroundBlurPrefValue(background_blur_state_in_pref)) {
    LOG(ERROR) << __FUNCTION__ << " background_blur_state_in_pref "
               << background_blur_state_in_pref
               << " is NOT a valid background blur effect state, using kOff";
    background_blur_state_in_pref = BackgroundBlurPrefValue::kOff;
  }

  CameraHalBackgroundBlurState blur_state =
      MapBackgroundBlurPrefValueToCameraHalState(background_blur_state_in_pref);
  effects->blur_enabled = blur_state.second;
  effects->blur_level = blur_state.first;

  if (is_eligible_for_background_replace_) {
    effects->replace_enabled =
        pref_change_registrar_->prefs()->GetBoolean(prefs::kBackgroundReplace);
    if (effects->replace_enabled) {
      effects->background_filepath =
          pref_change_registrar_->prefs()->GetFilePath(
              prefs::kBackgroundImagePath);
    }
  }

  effects->relight_enabled =
      pref_change_registrar_->prefs()->GetBoolean(prefs::kPortraitRelighting);
  effects->retouch_enabled =
      pref_change_registrar_->prefs()->GetBoolean(prefs::kFaceRetouch);
  effects->studio_look_enabled =
      pref_change_registrar_->prefs()->GetBoolean(prefs::kStudioLook);
  return effects;
}

void CameraEffectsController::SetEffectsConfigToPref(
    cros::mojom::EffectsConfigPtr new_config) {
  if (!pref_change_registrar_ || !pref_change_registrar_->prefs()) {
    return;
  }

  if (new_config->blur_enabled != current_effects_->blur_enabled ||
      new_config->blur_level != current_effects_->blur_level) {
    pref_change_registrar_->prefs()->SetInteger(
        prefs::kBackgroundBlur,
        MapBackgroundBlurCameraHalStateToPrefValue(new_config->blur_level,
                                                   new_config->blur_enabled));
  }

  if (is_eligible_for_background_replace_) {
    if (new_config->replace_enabled != current_effects_->replace_enabled) {
      pref_change_registrar_->prefs()->SetBoolean(prefs::kBackgroundReplace,
                                                  new_config->replace_enabled);
    }

    if (new_config->background_filepath !=
        current_effects_->background_filepath) {
      pref_change_registrar_->prefs()->SetFilePath(
          prefs::kBackgroundImagePath,
          new_config->background_filepath.value_or(base::FilePath()));
    }
  }

  if (new_config->relight_enabled != current_effects_->relight_enabled) {
    pref_change_registrar_->prefs()->SetBoolean(prefs::kPortraitRelighting,
                                                new_config->relight_enabled);
  }

  if (new_config->retouch_enabled != current_effects_->retouch_enabled) {
    pref_change_registrar_->prefs()->SetBoolean(prefs::kFaceRetouch,
                                                new_config->retouch_enabled);
  }

  if (new_config->studio_look_enabled !=
      current_effects_->studio_look_enabled) {
    pref_change_registrar_->prefs()->SetBoolean(
        prefs::kStudioLook, new_config->studio_look_enabled);
  }
}

bool CameraEffectsController::IsEffectControlAvailable(
    cros::mojom::CameraEffect effect /* = cros::mojom::CameraEffect::kNone*/) {
  switch (effect) {
    case cros::mojom::CameraEffect::kNone:
    case cros::mojom::CameraEffect::kBackgroundBlur:
      return features::IsVideoConferenceEnabled();
    case cros::mojom::CameraEffect::kPortraitRelight:
      return features::IsVcPortraitRelightEnabled();
    case cros::mojom::CameraEffect::kBackgroundReplace:
      return features::IsVcBackgroundReplaceEnabled();
  }
}

void CameraEffectsController::InitializeEffectControls() {
  if (VideoConferenceTrayController::Get()
          ->GetEffectsManager()
          .IsDelegateRegistered(this)) {
    return;
  }

  // If background blur UI controls are present, construct the effect and its
  // states.
  if (IsEffectControlAvailable(cros::mojom::CameraEffect::kBackgroundBlur)) {
    auto effect = std::make_unique<VcHostedEffect>(
        /*type=*/VcEffectType::kSetValue,
        /*get_state_callback=*/
        base::BindRepeating(&CameraEffectsController::GetEffectState,
                            base::Unretained(this),
                            VcEffectId::kBackgroundBlur),
        /*effect_id=*/VcEffectId::kBackgroundBlur);
    effect->set_label_text(l10n_util::GetStringUTF16(
        IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_NAME));
    effect->set_effects_delegate(this);
    AddBackgroundBlurStateToEffect(
        effect.get(), kVideoConferenceBackgroundBlurOffIcon,
        /*state_value=*/BackgroundBlurPrefValue::kOff,
        /*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_OFF,
        video_conference::BubbleViewID::kBackgroundBlurOffButton,
        /*is_disabled_by_enterprise=*/false);
    AddBackgroundBlurStateToEffect(
        effect.get(), kVideoConferenceBackgroundBlurLightIcon,
        /*state_value=*/BackgroundBlurPrefValue::kLight,
        /*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_LIGHT,
        video_conference::BubbleViewID::kBackgroundBlurLightButton,
        /*is_disabled_by_enterprise=*/false);
    AddBackgroundBlurStateToEffect(
        effect.get(), kVideoConferenceBackgroundBlurMaximumIcon,
        /*state_value=*/BackgroundBlurPrefValue::kMaximum,
        /*string_id=*/
        IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_FULL,
        video_conference::BubbleViewID::kBackgroundBlurFullButton,
        /*is_disabled_by_enterprise=*/false);

    if (is_eligible_for_background_replace_) {
      AddBackgroundBlurStateToEffect(
          effect.get(), kAiImageIcon,
          /*state_value=*/BackgroundBlurPrefValue::kImage,
          /*string_id=*/
          IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_IMAGE,
          video_conference::BubbleViewID::kBackgroundBlurImageButton,
          /*is_disabled_by_enterprise=*/
          is_background_replace_disabled_by_enterprise_);
    }
    effect->set_dependency_flags(VcHostedEffect::ResourceDependency::kCamera);
    AddEffect(std::move(effect));
  }

  // If portrait relight UI controls are present, construct the effect and its
  // state. If the Studio Look feature is available, the same UI control is used
  // for Studio Look.
  if (IsEffectControlAvailable(cros::mojom::CameraEffect::kPortraitRelight)) {
    auto effect_id = features::IsVcStudioLookEnabled()
                         ? VcEffectId::kStudioLook
                         : VcEffectId::kPortraitRelighting;
    std::unique_ptr<VcHostedEffect> effect = std::make_unique<VcHostedEffect>(
        /*type=*/VcEffectType::kToggle,
        /*get_state_callback=*/
        base::BindRepeating(&CameraEffectsController::GetEffectState,
                            base::Unretained(this), effect_id),
        effect_id);

    const base::CommandLine* command_line =
        base::CommandLine::ForCurrentProcess();
    std::string face_retouch_override = command_line->GetSwitchValueASCII(
        media::switches::kFaceRetouchOverride);
    bool show_studio_look_ui =
        face_retouch_override ==
            media::switches::kFaceRetouchForceEnabledWithRelighting ||
        face_retouch_override ==
            media::switches::kFaceRetouchForceEnabledWithoutRelighting ||
        features::IsVcStudioLookEnabled();

    auto effect_state = std::make_unique<VcEffectState>(
        /*icon=*/show_studio_look_ui ? &kVideoConferenceStudioLookIcon
                                     : &kVideoConferencePortraitRelightOnIcon,
        /*label_text=*/
        l10n_util::GetStringUTF16(
            show_studio_look_ui
                ? IDS_ASH_VIDEO_CONFERENCE_BUBBLE_STUDIO_LOOK_NAME
                : IDS_ASH_VIDEO_CONFERENCE_BUBBLE_PORTRAIT_RELIGHT_NAME),
        /*accessible_name_id=*/
        show_studio_look_ui
            ? IDS_ASH_VIDEO_CONFERENCE_BUBBLE_STUDIO_LOOK_NAME
            : IDS_ASH_VIDEO_CONFERENCE_BUBBLE_PORTRAIT_RELIGHT_NAME,
        /*button_callback=*/
        base::BindRepeating(&CameraEffectsController::OnEffectControlActivated,
                            base::Unretained(this), effect_id,
                            /*value=*/std::nullopt));
    effect->AddState(std::move(effect_state));

    effect->set_dependency_flags(VcHostedEffect::ResourceDependency::kCamera);
    AddEffect(std::move(effect));
  }

  // If *any* effects' UI controls are present, register with the effects
  // manager.
  if (IsEffectControlAvailable()) {
    VideoConferenceTrayController::Get()->GetEffectsManager().RegisterDelegate(
        this);
  }
}

void CameraEffectsController::AddBackgroundBlurStateToEffect(
    VcHostedEffect* effect,
    const gfx::VectorIcon& icon,
    int state_value,
    int string_id,
    int view_id,
    bool is_disabled_by_enterprise) {
  DCHECK(effect);
  effect->AddState(std::make_unique<VcEffectState>(
      &icon,
      /*label_text=*/l10n_util::GetStringUTF16(string_id),
      /*accessible_name_id=*/string_id,
      /*button_callback=*/
      base::BindRepeating(&CameraEffectsController::OnEffectControlActivated,
                          weak_factory_.GetWeakPtr(),
                          /*effect_id=*/VcEffectId::kBackgroundBlur,
                          /*value=*/state_value),
      /*state=*/state_value, view_id, is_disabled_by_enterprise));
}

void CameraEffectsController::SetCameraEffectsInCameraHalDispatcherImpl(
    cros::mojom::EffectsConfigPtr config) {
  // Directly calls the callback for testing case.
  if (in_testing_mode_) {
    CHECK_IS_TEST();
    OnCameraEffectChanged(std::move(config));
  } else {
    media::CameraHalDispatcherImpl::GetInstance()->SetCameraEffects(
        std::move(config));
  }
}

}  // namespace ash