// 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/constants/ash_switches.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/tab_slider.h"
#include "ash/style/tab_slider_button.h"
#include "ash/system/camera/autozoom_controller_impl.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/bubble/set_value_effects_view.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_tray.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "media/capture/video/chromeos/mojom/cros_camera_service.mojom-shared.h"
#include "media/capture/video/chromeos/mojom/effects_pipeline.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/jpeg_codec.h"
namespace ash {
using ::testing::ElementsAre;
using BackgroundImageInfo = CameraEffectsController::BackgroundImageInfo;
constexpr char kMetadataSuffix[] = ".metadata";
// Helper for converting `bitmap` into string.
std::string SkBitmapToString(const SkBitmap& bitmap) {
std::vector<unsigned char> data;
gfx::JPEGCodec::Encode(bitmap, /*quality=*/100, &data);
return std::string(data.begin(), data.end());
}
// Create fake Jpg image bytes.
std::string CreateJpgBytes(SkColor color) {
SkBitmap bitmap;
bitmap.allocN32Pixels(CameraEffectsController::kImageAsIconWidth,
CameraEffectsController::kImageAsIconWidth);
bitmap.eraseColor(color);
return SkBitmapToString(bitmap);
}
// Matcher defined to compare BackgroundImageInfo.
// We ignore the creation_time and last_accessed for now, because that were
// obtained from the filesystem, which is hard to mock for now.
auto BackgroundImageInfoMatcher(const base::FilePath& basename,
const std::string& jpeg_bytes,
const std::string& metadata) {
return testing::AllOf(
testing::Field("basename", &BackgroundImageInfo::basename,
testing::Eq(basename)),
testing::ResultOf(
[](BackgroundImageInfo info) {
info.image.SetReadOnly();
return SkBitmapToString(*info.image.bitmap());
},
testing::Eq(jpeg_bytes)),
testing::Field("metadata", &BackgroundImageInfo::metadata,
testing::Eq(metadata)));
}
constexpr char kTestAccount[] = "[email protected]";
class CameraEffectsControllerTest : public NoSessionAshTestBase {
public:
// NoSessionAshTestBase:
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(
features::kFeatureManagementVideoConference);
// Instantiates a fake controller (the real one is created in
// ChromeBrowserMainExtraPartsAsh::PreProfileInit() which is not called in
// ash unit tests).
controller_ = std::make_unique<FakeVideoConferenceTrayController>();
AshTestBase::SetUp();
camera_effects_controller_ = Shell::Get()->camera_effects_controller();
// Enable test mode to mock the SetCameraEffects calls.
camera_effects_controller_->bypass_set_camera_effects_for_testing(true);
// Create fake camera_background_img_dir_ and camera_background_run_dir_.
ASSERT_TRUE(file_tmp_dir_.CreateUniqueTempDir());
camera_background_img_dir_ =
file_tmp_dir_.GetPath().AppendASCII("camera_background_img_dir_");
camera_background_run_dir_ =
file_tmp_dir_.GetPath().AppendASCII("camera_background_run_dir_");
ASSERT_TRUE(base::CreateDirectory(camera_background_img_dir_));
ASSERT_TRUE(base::CreateDirectory(camera_background_run_dir_));
}
void TearDown() override {
NoSessionAshTestBase::TearDown();
controller_.reset();
}
// Sets background blur state.
void SetBackgroundBlurEffectState(std::optional<int> state) {
camera_effects_controller_->OnEffectControlActivated(
VcEffectId::kBackgroundBlur, state);
}
// Gets the state of the background blur effect from the effect's host,
// `camera_effects_controller_`.
int GetBackgroundBlurEffectState() {
std::optional<int> effect_state =
camera_effects_controller_->GetEffectState(VcEffectId::kBackgroundBlur);
DCHECK(effect_state.has_value());
return effect_state.value();
}
// Retrieves the value of `prefs::kBackgroundBlur`.
int GetBackgroundBlurPref() {
return Shell::Get()
->session_controller()
->GetActivePrefService()
->GetInteger(prefs::kBackgroundBlur);
}
// Returns a pair of <replace-enabled, background-image-filepath>.
std::pair<bool, std::string> GetBackgroundReplacePref() {
const auto* prefs =
Shell::Get()->session_controller()->GetActivePrefService();
return {prefs->GetBoolean(prefs::kBackgroundReplace),
prefs->GetFilePath(prefs::kBackgroundImagePath).value()};
}
// Simulates toggling portrait relighting effect state. Note that the `state`
// argument doesn't matter for toggle effects.
void TogglePortraitRelightingEffectState() {
camera_effects_controller_->OnEffectControlActivated(
VcEffectId::kPortraitRelighting, /*state=*/std::nullopt);
}
// Simulates toggling camera framing effect state. Note that the `state`
// argument doesn't matter for toggle effects.
void ToggleCameraFramingEffectState() {
camera_effects_controller_->OnEffectControlActivated(
VcEffectId::kCameraFraming, /*state=*/std::nullopt);
}
// Gets the state of the portrait relighting effect from the effect's host,
// `camera_effects_controller_`.
bool GetPortraitRelightingEffectState() {
std::optional<int> effect_state =
camera_effects_controller_->GetEffectState(
VcEffectId::kPortraitRelighting);
DCHECK(effect_state.has_value());
return static_cast<bool>(effect_state.value());
}
// Retrieves the value of `prefs::kPortraitRelighting`.
bool GetPortraitRelightingPref() {
return Shell::Get()
->session_controller()
->GetActivePrefService()
->GetBoolean(prefs::kPortraitRelighting);
}
void SetAutozoomSupportState(bool autozoom_supported) {
auto* autozoom_controller = Shell::Get()->autozoom_controller();
autozoom_controller->SetAutozoomSupported(autozoom_supported);
if (!autozoom_supported) {
return;
}
// Autozoom is only supported if there's an active camera client, so we
// simulate that here.
autozoom_controller->OnActiveClientChange(
cros::mojom::CameraClientType::ASH_CHROME,
/*is_new_active_client=*/true,
/*active_device_ids=*/{"fake_id"});
}
CameraEffectsController* camera_effects_controller() {
return camera_effects_controller_;
}
FakeVideoConferenceTrayController* tray_controller() {
return controller_.get();
}
base::FilePath GetFileInBackgroundRunDir() {
base::FileEnumerator enumerator(camera_background_run_dir_,
/*recursive=*/false,
base::FileEnumerator::FILES);
std::vector<base::FilePath> files;
for (auto path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
files.push_back(enumerator.GetInfo().GetName());
}
// We should always see 1 file in the camera_background_run_dir_.
CHECK_EQ(files.size(), 1u);
return files[0];
}
protected:
const SeaPenImage content1_ =
SeaPenImage(CreateJpgBytes(SK_ColorBLACK), 12345);
const SeaPenImage content2_ = SeaPenImage(CreateJpgBytes(SK_ColorWHITE), 888);
const std::string metadata1_ = "metadata1_";
const std::string metadata2_ = "metadata2_";
const base::FilePath filename1_ = base::FilePath("12345.jpg");
const base::FilePath filename2_ = base::FilePath("888.jpg");
const base::FilePath metadata_filename1_ =
filename1_.AddExtensionASCII(kMetadataSuffix);
base::FilePath metadata_filename2_ =
filename2_.AddExtensionASCII(kMetadataSuffix);
base::ScopedTempDir file_tmp_dir_;
base::FilePath camera_background_img_dir_;
base::FilePath camera_background_run_dir_;
raw_ptr<CameraEffectsController, DanglingUntriaged>
camera_effects_controller_ = nullptr;
std::unique_ptr<FakeVideoConferenceTrayController> controller_;
base::test::ScopedFeatureList scoped_feature_list_;
base::WeakPtrFactory<CameraEffectsControllerTest> weak_factory_{this};
};
TEST_F(CameraEffectsControllerTest, IsEffectControlAvailable) {
{
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{}, {features::kFeatureManagementVideoConference});
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundBlur));
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kPortraitRelight));
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundReplace));
}
{
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{features::kFeatureManagementVideoConference}, {});
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundBlur));
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kPortraitRelight));
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundReplace));
}
{
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{features::kFeatureManagementVideoConference},
{features::kVcPortraitRelight});
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundBlur));
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kPortraitRelight));
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundReplace));
}
{
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{features::kFeatureManagementVideoConference},
{features::kVcBackgroundReplace});
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundBlur));
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kPortraitRelight));
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundReplace));
}
}
TEST_F(CameraEffectsControllerTest, BackgroundBlurOnEffectControlActivated) {
SimulateUserLogin(kTestAccount);
// Activate the possible values of
// `CameraEffectsController::BackgroundBlurPrefValue`, verify that the pref
// and internal state are all set properly.
for (const auto state :
{CameraEffectsController::BackgroundBlurPrefValue::kOff,
CameraEffectsController::BackgroundBlurPrefValue::kLowest,
CameraEffectsController::BackgroundBlurPrefValue::kLight,
CameraEffectsController::BackgroundBlurPrefValue::kMedium,
CameraEffectsController::BackgroundBlurPrefValue::kHeavy,
CameraEffectsController::BackgroundBlurPrefValue::kMaximum}) {
SetBackgroundBlurEffectState(state);
EXPECT_EQ(GetBackgroundBlurPref(), state);
EXPECT_EQ(GetBackgroundBlurEffectState(), state);
}
// Invalid background blur effect state should set the state to kOff.
SetBackgroundBlurEffectState(100);
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
EXPECT_EQ(GetBackgroundBlurEffectState(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
// Set the background blur state to be kMaximum.
SetBackgroundBlurEffectState(
CameraEffectsController::BackgroundBlurPrefValue::kMaximum);
// Setting the background blur state to null will reset the effects as
// kOff.
SetBackgroundBlurEffectState(std::nullopt);
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
EXPECT_EQ(GetBackgroundBlurEffectState(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
}
TEST_F(CameraEffectsControllerTest,
PortraitRelightingOnEffectControlActivated) {
SimulateUserLogin(kTestAccount);
// Initial state should be "off".
EXPECT_FALSE(GetPortraitRelightingEffectState());
EXPECT_FALSE(GetPortraitRelightingPref());
// Activating the effect should toggle it to "true."
TogglePortraitRelightingEffectState();
EXPECT_TRUE(GetPortraitRelightingEffectState());
EXPECT_TRUE(GetPortraitRelightingPref());
// Another toggle should set it to "false."
TogglePortraitRelightingEffectState();
EXPECT_FALSE(GetPortraitRelightingEffectState());
EXPECT_FALSE(GetPortraitRelightingPref());
// And one more toggle should set it back to "true."
TogglePortraitRelightingEffectState();
EXPECT_TRUE(GetPortraitRelightingEffectState());
EXPECT_TRUE(GetPortraitRelightingPref());
}
TEST_F(CameraEffectsControllerTest, PrefOnCameraEffectChanged) {
SimulateUserLogin(kTestAccount);
// Initial state should be "off".
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
EXPECT_EQ(GetBackgroundBlurEffectState(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
EXPECT_FALSE(GetPortraitRelightingEffectState());
EXPECT_FALSE(GetPortraitRelightingPref());
// Case 1: when observe effects change from `CameraHalDispatcherImp`, the pref
// is updated.
cros::mojom::EffectsConfigPtr new_effects = cros::mojom::EffectsConfig::New();
new_effects->blur_enabled = true;
new_effects->blur_level = cros::mojom::BlurLevel::kMaximum;
new_effects->relight_enabled = true;
camera_effects_controller_->OnCameraEffectChanged(std::move(new_effects));
// State should be "on".
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kMaximum);
EXPECT_EQ(GetBackgroundBlurEffectState(),
CameraEffectsController::BackgroundBlurPrefValue::kMaximum);
EXPECT_TRUE(GetPortraitRelightingEffectState());
EXPECT_TRUE(GetPortraitRelightingPref());
// Case 2: when new effects is null, the pref is unchanged.
new_effects = cros::mojom::EffectsConfigPtr();
camera_effects_controller_->OnCameraEffectChanged(std::move(new_effects));
// State should be "on".
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kMaximum);
EXPECT_EQ(GetBackgroundBlurEffectState(),
CameraEffectsController::BackgroundBlurPrefValue::kMaximum);
EXPECT_TRUE(GetPortraitRelightingEffectState());
EXPECT_TRUE(GetPortraitRelightingPref());
// Case 3: when observe default effects from `CameraHalDispatcherImp`, the
// pref should be back to default.
new_effects = cros::mojom::EffectsConfig::New();
camera_effects_controller_->OnCameraEffectChanged(std::move(new_effects));
// State should be "off".
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
EXPECT_EQ(GetBackgroundBlurEffectState(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
EXPECT_FALSE(GetPortraitRelightingEffectState());
EXPECT_FALSE(GetPortraitRelightingPref());
}
TEST_F(CameraEffectsControllerTest, ResourceDependencyFlags) {
SimulateUserLogin(kTestAccount);
// Makes sure that all registered effects have the correct dependency flag.
auto* background_blur =
camera_effects_controller()->GetEffectById(VcEffectId::kBackgroundBlur);
EXPECT_EQ(VcHostedEffect::ResourceDependency::kCamera,
background_blur->dependency_flags());
auto* portrait_relight = camera_effects_controller()->GetEffectById(
VcEffectId::kPortraitRelighting);
EXPECT_EQ(VcHostedEffect::ResourceDependency::kCamera,
portrait_relight->dependency_flags());
}
TEST_F(CameraEffectsControllerTest, BackgroundBlurEnums) {
// This test makes sure that `BackgroundBlurState` and
// `BackgroundBlurPrefValue` is in sync with each other.
EXPECT_EQ(
static_cast<int>(CameraEffectsController::BackgroundBlurState::kMaximum),
CameraEffectsController::BackgroundBlurPrefValue::kMaximum + 1);
}
TEST_F(CameraEffectsControllerTest, BackgroundBlurMetricsRecord) {
base::HistogramTester histogram_tester;
SimulateUserLogin(kTestAccount);
// Update media status to make the video conference tray visible.
VideoConferenceMediaState state;
state.has_media_app = true;
state.has_camera_permission = true;
state.has_microphone_permission = true;
state.is_capturing_screen = true;
tray_controller()->UpdateWithMediaState(state);
auto* vc_tray = StatusAreaWidgetTestHelper::GetStatusAreaWidget()
->video_conference_tray();
// Open the vc bubble.
LeftClickOn(vc_tray->toggle_bubble_button());
// The set-value effects panel should have only 1 child view, and that view
// should be the slider associated to the background blur effect.
auto* set_value_effects_view = vc_tray->GetBubbleView()->GetViewByID(
video_conference::BubbleViewID::kSetValueEffectsView);
ASSERT_EQ(1u, set_value_effects_view->children().size());
auto* background_blur_slider =
static_cast<video_conference::SetValueEffectSlider*>(
set_value_effects_view->children()[0]);
EXPECT_EQ(VcEffectId::kBackgroundBlur, background_blur_slider->effect_id());
auto* tab_slider = background_blur_slider->tab_slider();
auto* first_button = tab_slider->GetButtonAtIndex(0);
// At first, the first button is selected, but there should not be any metrics
// recorded for that state since it is the default state when opening the
// bubble.
EXPECT_TRUE(first_button->selected());
histogram_tester.ExpectBucketCount(
"Ash.VideoConferenceTray.BackgroundBlur.Click",
CameraEffectsController::BackgroundBlurState::kOff, 0);
// Switching states by clicking slider buttons should record metrics in the
// associated bucket.
LeftClickOn(tab_slider->GetButtonAtIndex(1));
histogram_tester.ExpectBucketCount(
"Ash.VideoConferenceTray.BackgroundBlur.Click",
CameraEffectsController::BackgroundBlurState::kLight, 1);
LeftClickOn(tab_slider->GetButtonAtIndex(2));
histogram_tester.ExpectBucketCount(
"Ash.VideoConferenceTray.BackgroundBlur.Click",
CameraEffectsController::BackgroundBlurState::kMaximum, 1);
LeftClickOn(first_button);
histogram_tester.ExpectBucketCount(
"Ash.VideoConferenceTray.BackgroundBlur.Click",
CameraEffectsController::BackgroundBlurState::kOff, 1);
}
TEST_F(CameraEffectsControllerTest, CameraFramingSupportState) {
SimulateUserLogin(kTestAccount);
// By default autozoom is not supported, so the effect is not added.
EXPECT_FALSE(
camera_effects_controller()->GetEffectById(VcEffectId::kCameraFraming));
SetAutozoomSupportState(true);
EXPECT_TRUE(
camera_effects_controller()->GetEffectById(VcEffectId::kCameraFraming));
SetAutozoomSupportState(false);
EXPECT_FALSE(
camera_effects_controller()->GetEffectById(VcEffectId::kCameraFraming));
}
TEST_F(CameraEffectsControllerTest, CameraFramingToggle) {
SetAutozoomSupportState(true);
SimulateUserLogin(kTestAccount);
ASSERT_EQ(Shell::Get()->autozoom_controller()->GetState(),
cros::mojom::CameraAutoFramingState::OFF);
ToggleCameraFramingEffectState();
EXPECT_EQ(Shell::Get()->autozoom_controller()->GetState(),
cros::mojom::CameraAutoFramingState::ON_SINGLE);
ToggleCameraFramingEffectState();
EXPECT_EQ(Shell::Get()->autozoom_controller()->GetState(),
cros::mojom::CameraAutoFramingState::OFF);
}
TEST_F(CameraEffectsControllerTest, SetBackgroundImageWithFileExists) {
base::test::ScopedFeatureList scoped_feature_list{
features::kVcBackgroundReplace};
SimulateUserLogin(kTestAccount);
camera_effects_controller()->set_camera_background_img_dir_for_testing(
camera_background_img_dir_);
camera_effects_controller()->set_camera_background_run_dir_for_testing(
camera_background_run_dir_);
// Apply background blur first.
const auto state = CameraEffectsController::BackgroundBlurPrefValue::kLowest;
SetBackgroundBlurEffectState(state);
EXPECT_EQ(GetBackgroundBlurPref(), state);
// Create fake image file.
const std::string relative_path = "background/test.png";
base::FilePath file_fullpath =
camera_background_img_dir_.Append(relative_path);
ASSERT_TRUE(base::CreateDirectory(file_fullpath.DirName()));
ASSERT_TRUE(base::WriteFile(file_fullpath, ""));
// Set background image.
camera_effects_controller()->SetBackgroundImage(
base::FilePath(relative_path),
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// Check background replace result from pref.
EXPECT_THAT(GetBackgroundReplacePref(), testing::Pair(true, relative_path));
// Check the background blur is turned off.
EXPECT_EQ(GetBackgroundBlurPref(),
CameraEffectsController::BackgroundBlurPrefValue::kOff);
// Apply background blur again.
SetBackgroundBlurEffectState(state);
EXPECT_EQ(GetBackgroundBlurPref(), state);
// Background replace should be turned off.
EXPECT_THAT(GetBackgroundReplacePref(), testing::Pair(false, ""));
// When background replace is turned off, we want the background_filepath to
// be empty.
EXPECT_FALSE(camera_effects_controller()
->GetCameraEffects()
->background_filepath.has_value());
// Set background image again.
camera_effects_controller()->SetBackgroundImage(
base::FilePath(relative_path),
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// Check background replace result from pref.
EXPECT_THAT(GetBackgroundReplacePref(), testing::Pair(true, relative_path));
// Turn off backgroundblur or replace.
const auto off_state = CameraEffectsController::BackgroundBlurPrefValue::kOff;
SetBackgroundBlurEffectState(off_state);
EXPECT_EQ(GetBackgroundBlurPref(), off_state);
// Background replace should be turned off.
EXPECT_THAT(GetBackgroundReplacePref(), testing::Pair(false, ""));
}
TEST_F(CameraEffectsControllerTest, SetBackgroundImageWithFileDoesNotExist) {
base::test::ScopedFeatureList scoped_feature_list{
features::kVcBackgroundReplace};
SimulateUserLogin(kTestAccount);
camera_effects_controller()->set_camera_background_img_dir_for_testing(
camera_background_img_dir_);
camera_effects_controller()->set_camera_background_run_dir_for_testing(
camera_background_run_dir_);
// Apply background blur first.
const auto state = CameraEffectsController::BackgroundBlurPrefValue::kLowest;
SetBackgroundBlurEffectState(state);
EXPECT_EQ(GetBackgroundBlurPref(), state);
// Set background image.
camera_effects_controller()->SetBackgroundImage(
filename1_, base::BindOnce([](bool call_succeeded) {
EXPECT_FALSE(call_succeeded);
}));
task_environment()->RunUntilIdle();
// Because the image is not created, the above SetBackgroundImage should fail,
// so that the background replace pref should not be set.
EXPECT_THAT(GetBackgroundReplacePref(), testing::Pair(false, ""));
// Check the background blur is not changed.
EXPECT_EQ(GetBackgroundBlurPref(), state);
}
TEST_F(CameraEffectsControllerTest, SetBackgroundImageFromContent) {
base::test::ScopedFeatureList scoped_feature_list{
features::kVcBackgroundReplace};
SimulateUserLogin(kTestAccount);
camera_effects_controller()->set_camera_background_img_dir_for_testing(
camera_background_img_dir_);
camera_effects_controller()->set_camera_background_run_dir_for_testing(
camera_background_run_dir_);
// Set background image from content1_.
camera_effects_controller()->SetBackgroundImageFromContent(
content1_, metadata1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// We should see replace-enabled and the filename1_ in prefs.
EXPECT_THAT(GetBackgroundReplacePref(),
testing::Pair(true, filename1_.value()));
// Check saved image info.
camera_effects_controller()->GetRecentlyUsedBackgroundImages(
3, base::BindLambdaForTesting(
[&](const std::vector<BackgroundImageInfo>& info) {
EXPECT_THAT(info,
ElementsAre(BackgroundImageInfoMatcher(
filename1_, content1_.jpg_bytes, metadata1_)));
EXPECT_EQ(GetFileInBackgroundRunDir(), filename1_);
}));
task_environment()->RunUntilIdle();
// Set background image from content2_.
camera_effects_controller()->SetBackgroundImageFromContent(
content2_, metadata2_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// We should see replace-enabled and the filename2_ in prefs.
EXPECT_THAT(GetBackgroundReplacePref(),
testing::Pair(true, filename2_.value()));
camera_effects_controller()->GetRecentlyUsedBackgroundImages(
3, base::BindLambdaForTesting(
[&](const std::vector<BackgroundImageInfo>& info) {
EXPECT_THAT(
info, ElementsAre(
BackgroundImageInfoMatcher(
filename2_, content2_.jpg_bytes, metadata2_),
BackgroundImageInfoMatcher(
filename1_, content1_.jpg_bytes, metadata1_)));
EXPECT_EQ(GetFileInBackgroundRunDir(), filename2_);
}));
task_environment()->RunUntilIdle();
// SetBackgroundImage with filename1_ should update the last activity
// time of filename1_.
camera_effects_controller()->SetBackgroundImage(
filename1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// We should see replace-enabled and the filename1_ in prefs.
EXPECT_THAT(GetBackgroundReplacePref(),
testing::Pair(true, filename1_.value()));
camera_effects_controller()->GetRecentlyUsedBackgroundImages(
3, base::BindLambdaForTesting(
[&](const std::vector<BackgroundImageInfo>& info) {
EXPECT_THAT(
info, ElementsAre(
BackgroundImageInfoMatcher(
filename1_, content1_.jpg_bytes, metadata1_),
BackgroundImageInfoMatcher(
filename2_, content2_.jpg_bytes, metadata2_)));
EXPECT_EQ(GetFileInBackgroundRunDir(), filename1_);
}));
task_environment()->RunUntilIdle();
// Remove filename2_.
camera_effects_controller()->RemoveBackgroundImage(
filename2_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// Pref should not be effect if a irrelevant image is removed.
EXPECT_THAT(GetBackgroundReplacePref(),
testing::Pair(true, filename1_.value()));
camera_effects_controller()->GetRecentlyUsedBackgroundImages(
3, base::BindLambdaForTesting(
[&](const std::vector<BackgroundImageInfo>& info) {
EXPECT_THAT(info,
ElementsAre(BackgroundImageInfoMatcher(
filename1_, content1_.jpg_bytes, metadata1_)));
}));
task_environment()->RunUntilIdle();
// Remove filename1_.
camera_effects_controller()->RemoveBackgroundImage(
filename1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// Since filename2_ is removed, we should see the background image is removed
// from prefs.
EXPECT_THAT(GetBackgroundReplacePref(), testing::Pair(false, ""));
camera_effects_controller()->GetRecentlyUsedBackgroundImages(
3, base::BindLambdaForTesting(
[&](const std::vector<BackgroundImageInfo>& info) {
// We should only see all files removed.
EXPECT_TRUE(info.empty());
}));
task_environment()->RunUntilIdle();
}
TEST_F(CameraEffectsControllerTest, GetBackgroundImageFileNames) {
base::test::ScopedFeatureList scoped_feature_list{
features::kVcBackgroundReplace};
SimulateUserLogin(kTestAccount);
camera_effects_controller()->set_camera_background_img_dir_for_testing(
camera_background_img_dir_);
camera_effects_controller()->set_camera_background_run_dir_for_testing(
camera_background_run_dir_);
// Set background image from content1_.
camera_effects_controller()->SetBackgroundImageFromContent(
content1_, metadata1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// filename1_ should be created.
camera_effects_controller()->GetBackgroundImageFileNames(
base::BindLambdaForTesting([&](const std::vector<base::FilePath>& files) {
EXPECT_THAT(files, ElementsAre(filename1_));
}));
task_environment()->RunUntilIdle();
// Set background image from content2_.
camera_effects_controller()->SetBackgroundImageFromContent(
content2_, metadata2_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// filename2_ should be created.
camera_effects_controller()->GetBackgroundImageFileNames(
base::BindLambdaForTesting([&](const std::vector<base::FilePath>& files) {
EXPECT_THAT(files, ElementsAre(filename2_, filename1_));
}));
task_environment()->RunUntilIdle();
// SetBackgroundImage with filename1_ should update the last activity
// time of filename1_.
camera_effects_controller()->SetBackgroundImage(
filename1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// Returned files order should be changed.
camera_effects_controller()->GetBackgroundImageFileNames(
base::BindLambdaForTesting([&](const std::vector<base::FilePath>& files) {
EXPECT_THAT(files, ElementsAre(filename1_, filename2_));
}));
task_environment()->RunUntilIdle();
// Remove filename1_.
camera_effects_controller()->RemoveBackgroundImage(
filename1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// We should only see filename2_.
camera_effects_controller()->GetBackgroundImageFileNames(
base::BindLambdaForTesting([&](const std::vector<base::FilePath>& files) {
EXPECT_THAT(files, ElementsAre(filename2_));
}));
task_environment()->RunUntilIdle();
}
TEST_F(CameraEffectsControllerTest, GetBackgroundImageInfo) {
base::test::ScopedFeatureList scoped_feature_list{
features::kVcBackgroundReplace};
SimulateUserLogin(kTestAccount);
camera_effects_controller()->set_camera_background_img_dir_for_testing(
camera_background_img_dir_);
camera_effects_controller()->set_camera_background_run_dir_for_testing(
camera_background_run_dir_);
// Set background image from content1_.
camera_effects_controller()->SetBackgroundImageFromContent(
content1_, metadata1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
// GetBackgroundImageInfo should return info about filename1_.
camera_effects_controller()->GetBackgroundImageInfo(
filename1_, base::BindLambdaForTesting(
[&](const std::optional<BackgroundImageInfo>& info) {
EXPECT_THAT(
info.value(),
BackgroundImageInfoMatcher(
filename1_, content1_.jpg_bytes, metadata1_));
}));
task_environment()->RunUntilIdle();
// Delete metadata_filename1_ will return empty metadata.
camera_effects_controller()->RemoveBackgroundImage(
metadata_filename1_,
base::BindOnce([](bool call_succeeded) { EXPECT_TRUE(call_succeeded); }));
task_environment()->RunUntilIdle();
camera_effects_controller()->GetBackgroundImageInfo(
filename1_, base::BindLambdaForTesting(
[&](const std::optional<BackgroundImageInfo>& info) {
EXPECT_THAT(info.value(),
BackgroundImageInfoMatcher(
filename1_, content1_.jpg_bytes, ""));
}));
task_environment()->RunUntilIdle();
// GetBackgroundImageInfo should return nullopt for filename2_ because the
// file is not created.
camera_effects_controller()->GetBackgroundImageInfo(
filename2_,
base::BindOnce([](const std::optional<BackgroundImageInfo>& info) {
EXPECT_FALSE(info.has_value());
}));
task_environment()->RunUntilIdle();
}
TEST_F(CameraEffectsControllerTest, NotEligibleForSeaPen) {
// Set is_eligible_for_background_replace to false so that the image button
// will not be constructed.
GetSessionControllerClient()->set_is_eligible_for_background_replace(
{false, false});
SimulateUserLogin(kTestAccount);
// Update media status to make the video conference tray visible.
VideoConferenceMediaState state;
state.has_media_app = true;
state.has_camera_permission = true;
state.has_microphone_permission = true;
state.is_capturing_screen = true;
tray_controller()->UpdateWithMediaState(state);
auto effects = VideoConferenceTrayController::Get()
->GetEffectsManager()
.GetSetValueEffects();
EXPECT_EQ(effects.size(), 1u);
EXPECT_EQ(effects[0]->label_text(), u"Background");
// Verify that only three states are constructed; the forth one is the image
// button.
EXPECT_EQ(effects[0]->GetNumStates(), 3);
}
} // namespace ash