0

camera_effects: Host background blur effect

CameraEffectsController hosts the camera background blur effect,
exposing it via the VC bubble.

Bug: b/259585295
Change-Id: Ife30062e8482f094899a929e5506766a09455ba4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4119258
Commit-Queue: Roger Tinkoff <rtinkoff@google.com>
Reviewed-by: Alex Newcomer <newcomer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1088880}
This commit is contained in:
Roger Tinkoff
2023-01-04 19:49:02 +00:00
committed by Chromium LUCI CQ
parent 5d243e2d01
commit 473ee66968
19 changed files with 563 additions and 32 deletions

@ -1447,13 +1447,34 @@ Style notes:
Scanning
</message>
<!-- Video Conference tray-->
<!-- Video Conference tray/bubble -->
<message name="IDS_ASH_VIDEO_CONFERENCE_TOGGLE_BUBBLE_BUTTON_TOOLTIP" desc="The tooltip for the toggle bubble button in the video conference tray.">
Camera and audio controls
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SUMMARY_TEXT" desc="The summary text in the return to app panel of the video conference panel, specifying how many video conferencing apps are in used.">
Used by <ph name="APP_COUNT">$1<ex>2</ex></ph> apps
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_NAME" desc="Text for name of the background blur effect in the VC bubble.">
Background Blur
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_OFF" desc="Text for background blur 'off' setting, in the VC bubble.">
Off
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_LOWEST" desc="Text for background blur 'lowest' setting, in the VC bubble.">
Lowest
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_LIGHT" desc="Text for background blur 'light' setting, in the VC bubble.">
Light
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_MEDIUM" desc="Text for background blur 'medium' setting, in the VC bubble.">
Medium
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_HEAVY" desc="Text for background blur 'heavy' setting, in the VC bubble.">
Heavy
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_MAXIMUM" desc="Text for background blur 'maximum' setting, in the VC bubble.">
Maximum
</message>
<!-- Phone Hub tray-->
<message name="IDS_ASH_PHONE_HUB_TRAY_ACCESSIBLE_NAME" desc="The accessible name of the Phone Hub tray bubble for screen readers.">

@ -0,0 +1 @@
6244582b0cd0ef43796e6ff6e74058f7d6e90517

@ -0,0 +1 @@
6244582b0cd0ef43796e6ff6e74058f7d6e90517

@ -0,0 +1 @@
c79c38b0c43dc23f280615a1baa9ab6b0a1159fe

@ -0,0 +1 @@
6244582b0cd0ef43796e6ff6e74058f7d6e90517

@ -0,0 +1 @@
6244582b0cd0ef43796e6ff6e74058f7d6e90517

@ -0,0 +1 @@
6244582b0cd0ef43796e6ff6e74058f7d6e90517

@ -0,0 +1 @@
6244582b0cd0ef43796e6ff6e74058f7d6e90517

@ -5,23 +5,106 @@
#include "ash/system/camera/camera_effects_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.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_controller.h"
#include "base/check_is_test.h"
#include "base/notreached.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.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 "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace {
// The value stored in pref to indicate that background blur is disabled.
constexpr int kBackgroundBlurLevelForDisabling = -1;
// 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>;
// Returns 'true' if `pref_value` is an allowable value of
// `CameraEffectsController::BackgroundBlurEffectState`, 'false' otherwise.
bool IsValidBackgroundBlurState(int pref_value) {
switch (pref_value) {
case CameraEffectsController::BackgroundBlurEffectState::kOff:
case CameraEffectsController::BackgroundBlurEffectState::kLowest:
case CameraEffectsController::BackgroundBlurEffectState::kLight:
case CameraEffectsController::BackgroundBlurEffectState::kMedium:
case CameraEffectsController::BackgroundBlurEffectState::kHeavy:
case CameraEffectsController::BackgroundBlurEffectState::kMaximum:
return true;
}
return false;
}
// Maps `effect_state` (assumed to be a value read out of
// `prefs::kBackgroundBlur`) to a `CameraHalBackgroundBlurState` (that
// `CameraHalDispatcherImpl` expects).
CameraHalBackgroundBlurState MapBackgroundBlurEffectStateToCameraHalState(
int effect_state) {
DCHECK(IsValidBackgroundBlurState(effect_state));
switch (effect_state) {
// 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::BackgroundBlurEffectState::kOff:
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 `effect_state` is mapped to a `BlurLevel`.
case CameraEffectsController::BackgroundBlurEffectState::kLowest:
return std::make_pair(cros::mojom::BlurLevel::kLowest, true);
case CameraEffectsController::BackgroundBlurEffectState::kLight:
return std::make_pair(cros::mojom::BlurLevel::kLight, true);
case CameraEffectsController::BackgroundBlurEffectState::kMedium:
return std::make_pair(cros::mojom::BlurLevel::kMedium, true);
case CameraEffectsController::BackgroundBlurEffectState::kHeavy:
return std::make_pair(cros::mojom::BlurLevel::kHeavy, true);
case CameraEffectsController::BackgroundBlurEffectState::kMaximum:
return std::make_pair(cros::mojom::BlurLevel::kMaximum, true);
}
NOTREACHED();
return std::make_pair(cros::mojom::BlurLevel::kLowest, false);
}
// Maps the `CameraHalDispatcherImpl`-ready background blur state
// `level`/`enabled` to `CameraEffectsController::BackgroundBlurEffectState`,
// which is what's written to `prefs::kBackgroundBlur`.
CameraEffectsController::BackgroundBlurEffectState
MapBackgroundBlurCameraHalStateToEffectState(cros::mojom::BlurLevel level,
bool enabled) {
if (!enabled) {
return CameraEffectsController::BackgroundBlurEffectState::kOff;
}
switch (level) {
case cros::mojom::BlurLevel::kLowest:
return CameraEffectsController::BackgroundBlurEffectState::kLowest;
case cros::mojom::BlurLevel::kLight:
return CameraEffectsController::BackgroundBlurEffectState::kLight;
case cros::mojom::BlurLevel::kMedium:
return CameraEffectsController::BackgroundBlurEffectState::kMedium;
case cros::mojom::BlurLevel::kHeavy:
return CameraEffectsController::BackgroundBlurEffectState::kHeavy;
case cros::mojom::BlurLevel::kMaximum:
return CameraEffectsController::BackgroundBlurEffectState::kMaximum;
}
NOTREACHED();
return CameraEffectsController::BackgroundBlurEffectState::kLowest;
}
// Gets blur level from chrome flag.
cros::mojom::BlurLevel GetBlurLevelFromFlag() {
@ -83,7 +166,15 @@ CameraEffectsController::CameraEffectsController() {
weak_factory_.GetWeakPtr())));
}
CameraEffectsController::~CameraEffectsController() = default;
CameraEffectsController::~CameraEffectsController() {
VideoConferenceTrayEffectsManager& effects_manager =
VideoConferenceTrayController::Get()->effects_manager();
if (effects_manager.IsDelegateRegistered(this)) {
// The `VcEffectsDelegate` was registered, so must therefore be
// unregistered.
effects_manager.UnregisterDelegate(this);
}
}
bool CameraEffectsController::IsCameraEffectsSupported(
cros::mojom::CameraEffect effect) {
@ -118,15 +209,16 @@ void CameraEffectsController::RemoveObserver(Observer* observer) {
// static
void CameraEffectsController::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
if (!IsCameraEffectsSupported())
if (!IsCameraEffectsSupported()) {
return;
}
// We have to register all camera effects prefs; because we need use them to
// construct the cros::mojom::EffectsConfigPtr.
registry->RegisterIntegerPref(prefs::kBackgroundBlur,
GetInitialCameraEffects()->blur_enabled
? static_cast<int>(GetBlurLevelFromFlag())
: kBackgroundBlurLevelForDisabling);
: BackgroundBlurEffectState::kOff);
registry->RegisterBooleanPref(prefs::kBackgroundReplace,
GetInitialCameraEffects()->replace_enabled);
@ -137,8 +229,10 @@ void CameraEffectsController::RegisterProfilePrefs(
void CameraEffectsController::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
if (pref_change_registrar_ && pref_service == pref_change_registrar_->prefs())
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>();
@ -167,6 +261,50 @@ void CameraEffectsController::OnActiveUserPrefServiceChanged(
// 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());
// If any effects have controls the user can access, this will create the
// effects UI and register `CameraEffectsController`'s `VcEffectsDelegate`
// interface.
InitializeEffectControls();
}
int CameraEffectsController::GetEffectState(int effect_id) {
switch (effect_id) {
case static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur):
DCHECK(pref_change_registrar_ && pref_change_registrar_->prefs());
int pref_value =
pref_change_registrar_->prefs()->GetInteger(prefs::kBackgroundBlur);
if (!IsValidBackgroundBlurState(pref_value)) {
LOG(ERROR)
<< __FUNCTION__ << " value " << pref_value
<< " is NOT a valid background blur effect state, returning kOff";
return static_cast<int>(BackgroundBlurEffectState::kOff);
}
return pref_value;
}
return VcEffectState::kUnusedId;
}
void CameraEffectsController::OnEffectControlActivated(int effect_id,
int value) {
switch (effect_id) {
case static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur):
DCHECK(pref_change_registrar_ && pref_change_registrar_->prefs());
if (!IsValidBackgroundBlurState(value)) {
LOG(ERROR) << __FUNCTION__ << " value " << value
<< " is not a valid background blur effect state";
pref_change_registrar_->prefs()->SetInteger(
prefs::kBackgroundBlur,
static_cast<int>(BackgroundBlurEffectState::kOff));
return;
}
pref_change_registrar_->prefs()->SetInteger(prefs::kBackgroundBlur,
value);
return;
}
}
void CameraEffectsController::OnCameraEffectsPrefChanged(
@ -264,16 +402,21 @@ void CameraEffectsController::OnNewCameraEffectsSet(
cros::mojom::EffectsConfigPtr
CameraEffectsController::GetEffectsConfigFromPref() {
cros::mojom::EffectsConfigPtr effects = cros::mojom::EffectsConfig::New();
const int background_level_in_pref =
int background_blur_state_in_pref =
pref_change_registrar_->prefs()->GetInteger(prefs::kBackgroundBlur);
if (!IsValidBackgroundBlurState(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 =
static_cast<int>(BackgroundBlurEffectState::kOff);
}
effects->blur_enabled =
background_level_in_pref != kBackgroundBlurLevelForDisabling;
effects->blur_level =
effects->blur_enabled
? static_cast<cros::mojom::BlurLevel>(background_level_in_pref)
: GetBlurLevelFromFlag();
CameraHalBackgroundBlurState blur_state =
MapBackgroundBlurEffectStateToCameraHalState(
background_blur_state_in_pref);
effects->blur_enabled = blur_state.second;
effects->blur_level = blur_state.first;
effects->replace_enabled =
pref_change_registrar_->prefs()->GetBoolean(prefs::kBackgroundReplace);
@ -289,9 +432,9 @@ void CameraEffectsController::SetEffectsConfigToPref(
if (new_config->blur_enabled != old_effects->blur_enabled ||
new_config->blur_level != old_effects->blur_level) {
pref_change_registrar_->prefs()->SetInteger(
prefs::kBackgroundBlur, new_config->blur_enabled
? static_cast<int>(new_config->blur_level)
: kBackgroundBlurLevelForDisabling);
prefs::kBackgroundBlur,
MapBackgroundBlurCameraHalStateToEffectState(new_config->blur_level,
new_config->blur_enabled));
}
if (new_config->replace_enabled != old_effects->replace_enabled) {
@ -305,4 +448,100 @@ void CameraEffectsController::SetEffectsConfigToPref(
}
}
bool CameraEffectsController::IsEffectControlAvailable(
cros::mojom::CameraEffect effect /* = cros::mojom::CameraEffect::kNone*/) {
if (!ash::features::IsVcControlsUiEnabled()) {
return false;
}
switch (effect) {
case cros::mojom::CameraEffect::kNone:
// Return 'true' if any effect is available.
return IsCameraEffectsSupported(
cros::mojom::CameraEffect::kBackgroundBlur);
case cros::mojom::CameraEffect::kBackgroundBlur:
return IsCameraEffectsSupported(
cros::mojom::CameraEffect::kBackgroundBlur);
case cros::mojom::CameraEffect::kBackgroundReplace:
case cros::mojom::CameraEffect::kPortraitRelight:
return false;
}
return false;
}
void CameraEffectsController::InitializeEffectControls() {
if (VideoConferenceTrayController::Get()
->effects_manager()
.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>(
VcEffectType::kSetValue,
base::BindRepeating(
&CameraEffectsController::GetEffectState, base::Unretained(this),
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)));
effect->set_label_text(l10n_util::GetStringUTF16(
IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_NAME));
effect->set_id(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur));
AddBackgroundBlurStateToEffect(
effect.get(),
/*state_value=*/BackgroundBlurEffectState::kOff,
/*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_OFF);
AddBackgroundBlurStateToEffect(
effect.get(),
/*state_value=*/BackgroundBlurEffectState::kLowest,
/*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_LOWEST);
AddBackgroundBlurStateToEffect(
effect.get(),
/*state_value=*/BackgroundBlurEffectState::kLight,
/*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_LIGHT);
AddBackgroundBlurStateToEffect(
effect.get(),
/*state_value=*/BackgroundBlurEffectState::kMedium,
/*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_MEDIUM);
AddBackgroundBlurStateToEffect(
effect.get(),
/*state_value=*/BackgroundBlurEffectState::kHeavy,
/*string_id=*/IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_HEAVY);
AddBackgroundBlurStateToEffect(
effect.get(),
/*state_value=*/BackgroundBlurEffectState::kMaximum,
/*string_id=*/
IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_MAXIMUM);
AddEffect(std::move(effect));
}
// If *any* effects' UI controls are present, register with the effects
// manager.
if (IsEffectControlAvailable()) {
VideoConferenceTrayController::Get()->effects_manager().RegisterDelegate(
this);
}
}
void CameraEffectsController::AddBackgroundBlurStateToEffect(
VcHostedEffect* effect,
int state_value,
int string_id) {
DCHECK(effect);
effect->AddState(std::make_unique<VcEffectState>(
/*icon=*/nullptr,
/*label_text=*/l10n_util::GetStringUTF16(string_id),
/*accessible_name_id=*/string_id,
/*button_callback=*/
base::BindRepeating(
&CameraEffectsController::OnEffectControlActivated,
weak_factory_.GetWeakPtr(),
/*effect_id=*/
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
/*value=*/state_value),
/*state=*/state_value));
}
} // namespace ash

@ -10,12 +10,12 @@
#include "ash/ash_export.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_delegate.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "base/scoped_observation.h"
#include "media/capture/video/chromeos/mojom/effects_pipeline.mojom.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
class PrefRegistrySimple;
class PrefService;
@ -25,7 +25,8 @@ namespace ash {
// CameraEffectsController is the interface for any object in ash to
// enable/change camera effects.
class ASH_EXPORT CameraEffectsController : public SessionObserver {
class ASH_EXPORT CameraEffectsController : public SessionObserver,
public VcEffectsDelegate {
public:
// Observer that will be notified on camera effects change.
class Observer : public base::CheckedObserver {
@ -34,6 +35,17 @@ class ASH_EXPORT CameraEffectsController : public SessionObserver {
cros::mojom::EffectsConfigPtr new_effects) = 0;
};
// Enum that represents the value persisted to `prefs::kBackgroundBlur`,
// which is the "ultimate source of truth" for the background blur setting.
enum BackgroundBlurEffectState {
kOff = -1,
kLowest = 0,
kLight = 1,
kMedium = 2,
kHeavy = 3,
kMaximum = 4,
};
CameraEffectsController();
CameraEffectsController(const CameraEffectsController&) = delete;
@ -48,6 +60,11 @@ class ASH_EXPORT CameraEffectsController : public SessionObserver {
static bool IsCameraEffectsSupported(
cros::mojom::CameraEffect effect = cros::mojom::CameraEffect::kNone);
// Returns 'true' if UI controls for `effect` are available to the user,
// 'false' otherwise.
bool IsEffectControlAvailable(
cros::mojom::CameraEffect effect = cros::mojom::CameraEffect::kNone);
// Returns currently applied camera effects.
// Should only be called after user logs in.
cros::mojom::EffectsConfigPtr GetCameraEffects();
@ -62,6 +79,10 @@ class ASH_EXPORT CameraEffectsController : public SessionObserver {
// SessionObserver:
void OnActiveUserPrefServiceChanged(PrefService* pref_service) override;
// VcEffectsDelegate:
int GetEffectState(int effect_id) override;
void OnEffectControlActivated(int effect_id, int value) override;
void set_effect_result_for_testing(
cros::mojom::SetEffectResult effect_result_for_testing) {
effect_result_for_testing_ = effect_result_for_testing;
@ -89,6 +110,16 @@ class ASH_EXPORT CameraEffectsController : public SessionObserver {
// Update prefs with the value in `config`.
void SetEffectsConfigToPref(cros::mojom::EffectsConfigPtr config);
// Performs any initializations needed for effects whose controls are exposed
// via the UI.
void InitializeEffectControls();
// Adds a `std::unique_ptr<VcEffectState>` to `effect`, where `effect` is
// assumed to be that of camera background blur.
void AddBackgroundBlurStateToEffect(VcHostedEffect* effect,
int state_value,
int string_id);
// Used to bypass the CameraHalDispatcherImpl::SetCameraEffects for testing
// purpose. The value will be null for non-testing cases; and not null in
// testing cases.

@ -8,6 +8,7 @@
#include "ash/constants/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
@ -37,9 +38,22 @@ class CameraEffectsControllerTest : public NoSessionAshTestBase {
// NoSessionAshTestBase:
void SetUp() override {
scoped_feature_list_.InitWithFeatures(
{features::kVCBackgroundBlur},
{features::kVcControlsUi, features::kVCBackgroundBlur},
{features::kVCBackgroundReplace, features::kVCPortraitRelighting});
// Here we have to create the global instance of `CrasAudioHandler` before
// `FakeVideoConferenceTrayController`, so we do it here and not in
// `AshTestBase`.
CrasAudioClient::InitializeFake();
CrasAudioHandler::InitializeForTesting();
// 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>();
set_create_global_cras_audio_handler(false);
AshTestBase::SetUp();
camera_effects_controller_ = Shell::Get()->camera_effects_controller();
@ -49,6 +63,13 @@ class CameraEffectsControllerTest : public NoSessionAshTestBase {
cros::mojom::SetEffectResult::kOk);
}
void TearDown() override {
NoSessionAshTestBase::TearDown();
controller_.reset();
CrasAudioHandler::Shutdown();
CrasAudioClient::Shutdown();
}
// Enables/Disables pref values.
void SetEnabledPref(const std::string& perf_name, bool enabled) {
Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
@ -61,8 +82,21 @@ class CameraEffectsControllerTest : public NoSessionAshTestBase {
prefs::kBackgroundBlur, level);
}
// Retrieves the value of `prefs::kBackgroundBlur`.
int GetBackgroundBlurPref() {
return Shell::Get()
->session_controller()
->GetActivePrefService()
->GetInteger(prefs::kBackgroundBlur);
}
CameraEffectsController* camera_effects_controller() {
return camera_effects_controller_;
}
protected:
CameraEffectsController* camera_effects_controller_ = nullptr;
std::unique_ptr<FakeVideoConferenceTrayController> controller_;
base::test::ScopedFeatureList scoped_feature_list_;
};
@ -82,12 +116,19 @@ TEST_F(CameraEffectsControllerTest,
cros::mojom::CameraEffect::kBackgroundReplace));
EXPECT_FALSE(CameraEffectsController::IsCameraEffectsSupported(
cros::mojom::CameraEffect::kPortraitRelight));
// No camera effects supported and VC controls UI not enabled, no camera
// effects UI controls available.
EXPECT_TRUE(camera_effects_controller());
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable());
EXPECT_FALSE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundBlur));
}
{
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{features::kVCBackgroundBlur},
{features::kVcControlsUi, features::kVCBackgroundBlur},
{features::kVCBackgroundReplace, features::kVCPortraitRelighting});
// BackgroundBlur should be supported.
@ -98,6 +139,13 @@ TEST_F(CameraEffectsControllerTest,
cros::mojom::CameraEffect::kBackgroundReplace));
EXPECT_FALSE(CameraEffectsController::IsCameraEffectsSupported(
cros::mojom::CameraEffect::kPortraitRelight));
// Camera effects are supported, VC controls UI enabled, so camera effects
// UI controls are available, and background blur UI is available.
EXPECT_TRUE(camera_effects_controller());
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable());
EXPECT_TRUE(camera_effects_controller()->IsEffectControlAvailable(
cros::mojom::CameraEffect::kBackgroundBlur));
}
{
@ -445,5 +493,90 @@ TEST_F(CameraEffectsControllerTest, NotifyObserverTest) {
camera_effects_controller_->RemoveObserver(&observer);
}
TEST_F(CameraEffectsControllerTest, BackgroundBlurGetEffectState) {
SimulateUserLogin("testuser@gmail.com");
// Pref value is `kBackgroundBlurLevelForDisabling` (off).
SetBackgroundBlurPref(-1);
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kOff);
// Test all values of `cros::mojom::BlurLevel`.
SetBackgroundBlurPref(static_cast<int>(cros::mojom::BlurLevel::kLowest));
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kLowest);
SetBackgroundBlurPref(static_cast<int>(cros::mojom::BlurLevel::kLight));
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kLight);
SetBackgroundBlurPref(static_cast<int>(cros::mojom::BlurLevel::kMedium));
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kMedium);
SetBackgroundBlurPref(static_cast<int>(cros::mojom::BlurLevel::kHeavy));
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kHeavy);
SetBackgroundBlurPref(static_cast<int>(cros::mojom::BlurLevel::kMaximum));
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kMaximum);
// Now verify with a pref value that isn't recognized as a valid background
// blur state.
SetBackgroundBlurPref(-999);
EXPECT_EQ(camera_effects_controller_->GetEffectState(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur)),
CameraEffectsController::BackgroundBlurEffectState::kOff);
}
TEST_F(CameraEffectsControllerTest, BackgroundBlurOnEffectControlActivated) {
SimulateUserLogin("testuser@gmail.com");
// Activate `kOff`, verify that pref value is -1
// (`kBackgroundBlurLevelForDisabling`).
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
CameraEffectsController::BackgroundBlurEffectState::kOff);
EXPECT_EQ(GetBackgroundBlurPref(), -1);
// Activate the rest of the possible values of
// `CameraEffectsController::BackgroundBlurEffectState`, verify that the pref
// value is the expected value of `cros::mojom::BlurLevel`.
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
CameraEffectsController::BackgroundBlurEffectState::kLowest);
EXPECT_EQ(GetBackgroundBlurPref(),
static_cast<int>(cros::mojom::BlurLevel::kLowest));
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
CameraEffectsController::BackgroundBlurEffectState::kLight);
EXPECT_EQ(GetBackgroundBlurPref(),
static_cast<int>(cros::mojom::BlurLevel::kLight));
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
CameraEffectsController::BackgroundBlurEffectState::kMedium);
EXPECT_EQ(GetBackgroundBlurPref(),
static_cast<int>(cros::mojom::BlurLevel::kMedium));
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
CameraEffectsController::BackgroundBlurEffectState::kHeavy);
EXPECT_EQ(GetBackgroundBlurPref(),
static_cast<int>(cros::mojom::BlurLevel::kHeavy));
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur),
CameraEffectsController::BackgroundBlurEffectState::kMaximum);
EXPECT_EQ(GetBackgroundBlurPref(),
static_cast<int>(cros::mojom::BlurLevel::kMaximum));
// Passing an invalid background blur state is the same as activating
// `kOff`.
camera_effects_controller_->OnEffectControlActivated(
static_cast<int>(cros::mojom::CameraEffect::kBackgroundBlur), -999);
EXPECT_EQ(GetBackgroundBlurPref(), -1);
}
} // namespace
} // namespace ash

@ -22,6 +22,10 @@ enum BubbleViewID {
// `kMainBubbleView`.
kSetValueEffectsView,
// Container view for a single "set-value" VC effect, a child of
// `kSetValueEffectsView`.
kSingleSetValueEffectView,
// Container view for all "toggle" VC effects, a child of `kMainBubbleView`.
kToggleEffectsView,

@ -47,6 +47,8 @@ class BubbleViewTest : public AshTestBase {
office_bunny_ =
std::make_unique<fake_video_conference::OfficeBunnyEffect>();
shaggy_fur_ = std::make_unique<fake_video_conference::ShaggyFurEffect>();
super_cuteness_ =
std::make_unique<fake_video_conference::SuperCutnessEffect>();
set_create_global_cras_audio_handler(false);
AshTestBase::SetUp();
@ -59,6 +61,7 @@ class BubbleViewTest : public AshTestBase {
AshTestBase::TearDown();
office_bunny_.reset();
shaggy_fur_.reset();
super_cuteness_.reset();
controller_.reset();
CrasAudioHandler::Shutdown();
CrasAudioClient::Shutdown();
@ -98,6 +101,11 @@ class BubbleViewTest : public AshTestBase {
video_conference::BubbleViewID::kSetValueEffectsView);
}
views::View* single_set_value_effect_view() {
return bubble_view()->GetViewByID(
video_conference::BubbleViewID::kSingleSetValueEffectView);
}
views::View* return_to_app() {
return bubble_view()->GetViewByID(
video_conference::BubbleViewID::kReturnToApp);
@ -116,11 +124,17 @@ class BubbleViewTest : public AshTestBase {
return shaggy_fur_.get();
}
ash::fake_video_conference::SuperCutnessEffect* super_cuteness() {
return super_cuteness_.get();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<FakeVideoConferenceTrayController> controller_;
std::unique_ptr<ash::fake_video_conference::OfficeBunnyEffect> office_bunny_;
std::unique_ptr<ash::fake_video_conference::ShaggyFurEffect> shaggy_fur_;
std::unique_ptr<ash::fake_video_conference::SuperCutnessEffect>
super_cuteness_;
};
TEST_F(BubbleViewTest, NoEffects) {
@ -274,4 +288,42 @@ TEST_F(BubbleViewTest, SetValueButtonClicked) {
EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(1), 1);
EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(0), 1);
}
TEST_F(BubbleViewTest, ValidEffectState) {
// Verify that the delegate hosts a single effect which has at least two
// values.
EXPECT_EQ(super_cuteness()->GetNumEffects(), 1);
EXPECT_GE(super_cuteness()->GetEffect(0)->GetNumStates(), 2);
// Add one set-value effect.
controller()->effects_manager().RegisterDelegate(super_cuteness());
// Effect will NOT return an invalid state.
super_cuteness()->set_has_invalid_effect_state_for_testing(false);
// Click to open the bubble, a single set-value effect view is
// present/visible.
LeftClickOn(toggle_bubble_button());
views::View* effect_view = single_set_value_effect_view();
EXPECT_TRUE(effect_view);
EXPECT_TRUE(effect_view->GetVisible());
}
TEST_F(BubbleViewTest, InvalidEffectState) {
// Verify that the delegate hosts a single effect which has at least two
// values.
EXPECT_EQ(super_cuteness()->GetNumEffects(), 1);
EXPECT_GE(super_cuteness()->GetEffect(0)->GetNumStates(), 2);
// Add one set-value effect.
controller()->effects_manager().RegisterDelegate(super_cuteness());
// Effect WILL return an invalid state.
super_cuteness()->set_has_invalid_effect_state_for_testing(true);
// Click to open the bubble, a single set-value effect view is NOT present.
LeftClickOn(toggle_bubble_button());
EXPECT_FALSE(single_set_value_effect_view());
}
} // namespace ash::video_conference

@ -24,6 +24,7 @@ namespace {
class ValueButtonContainer : public views::View {
public:
explicit ValueButtonContainer(const VcHostedEffect* effect) {
SetID(BubbleViewID::kSingleSetValueEffectView);
views::FlexLayout* layout =
SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical);
@ -34,8 +35,13 @@ class ValueButtonContainer : public views::View {
AddChildView(std::make_unique<views::Label>(effect->label_text()));
}
// Add a button for each state.
// `effect` is expected to provide the current state of the effect, and
// a `current_state` of `VcEffectState::kUnusedId` means it couldn't be
// obtained.
const int current_state = effect->get_state_callback().Run();
DCHECK(current_state != VcEffectState::kUnusedId);
// Add a button for each state.
for (int i = 0; i < effect->GetNumStates(); ++i) {
const VcEffectState* state = effect->GetState(/*index=*/i);
std::unique_ptr<views::RadioButton> state_button =
@ -83,6 +89,15 @@ SetValueEffectsView::SetValueEffectsView(
if (controller->effects_manager().HasSetValueEffects()) {
for (auto* effect : controller->effects_manager().GetSetValueEffects()) {
// Make sure the current value of the effect can be obtained, and if it
// can't then don't present its controls.
if (effect->get_state_callback().Run() == VcEffectState::kUnusedId) {
LOG(ERROR) << __FUNCTION__ << " effect with id (" << effect->id()
<< ") label_text (" << effect->label_text()
<< ") could not obtain its current value";
continue;
}
AddChildView(std::make_unique<ValueButtonContainer>(effect));
}
}

@ -170,6 +170,10 @@ SuperCutnessEffect::SuperCutnessEffect() {
SuperCutnessEffect::~SuperCutnessEffect() = default;
int SuperCutnessEffect::GetEffectState(int effect_id) {
if (has_invalid_effect_state_for_testing_) {
return VcEffectState::kUnusedId;
}
return static_cast<int>(HowCute::kTeddyBear);
}

@ -189,6 +189,13 @@ class ASH_EXPORT SuperCutnessEffect : public VcEffectsDelegate {
// activated.
int GetNumActivationsForTesting(int value);
void set_has_invalid_effect_state_for_testing(bool has_invalid_state) {
has_invalid_effect_state_for_testing_ = has_invalid_state;
}
bool has_invalid_effect_state_for_testing() {
return has_invalid_effect_state_for_testing_;
}
private:
// Adds a `std::unique_ptr<VcEffectState>` to `effect`.
void AddStateToEffect(VcHostedEffect* effect,
@ -199,6 +206,10 @@ class ASH_EXPORT SuperCutnessEffect : public VcEffectsDelegate {
// `HowCute`.
std::vector<int> num_activations_for_testing_;
// Set to 'true' for testing the case where a valid effect state cannot be
// obtained.
bool has_invalid_effect_state_for_testing_;
base::WeakPtrFactory<SuperCutnessEffect> weak_factory_{this};
};

@ -47,7 +47,9 @@ class ASH_EXPORT VcEffectsDelegate {
// Invoked when the UI controls are being constructed, to get the actual
// effect state. `effect_id` specifies the effect whose state is requested,
// and can be ignored if only one effect is being hosted.
// and can be ignored if only one effect is being hosted. If no state can be
// determined for `effect_id`, this function should return
// `VcEffectState::kUnusedId`.
virtual int GetEffectState(int effect_id) = 0;
// Invoked anytime the user makes an adjustment. `effect_id` is the unique ID

@ -21,10 +21,7 @@ VideoConferenceTrayEffectsManager::~VideoConferenceTrayEffectsManager() =
void VideoConferenceTrayEffectsManager::RegisterDelegate(
VcEffectsDelegate* delegate) {
DCHECK(delegate);
DCHECK(std::find_if(effect_delegates_.begin(), effect_delegates_.end(),
[delegate](VcEffectsDelegate* d) {
return delegate == d;
}) == effect_delegates_.end());
DCHECK(!IsDelegateRegistered(delegate));
effect_delegates_.push_back(delegate);
}
@ -37,6 +34,15 @@ void VideoConferenceTrayEffectsManager::UnregisterDelegate(
DCHECK_EQ(num_items_erased, 1UL);
}
bool VideoConferenceTrayEffectsManager::IsDelegateRegistered(
VcEffectsDelegate* delegate) {
DCHECK(delegate);
return std::find_if(effect_delegates_.begin(), effect_delegates_.end(),
[delegate](VcEffectsDelegate* d) {
return delegate == d;
}) != effect_delegates_.end();
}
bool VideoConferenceTrayEffectsManager::HasToggleEffects() {
return GetTotalToggleEffectButtons().size() > 0;
}
@ -49,8 +55,9 @@ VideoConferenceTrayEffectsManager::GetToggleEffectButtonTable() {
EffectDataTable buttons;
int num_buttons = total_buttons.size();
if (num_buttons == 0)
if (num_buttons == 0) {
return buttons;
}
if (num_buttons <= 3) {
// For 3 or fewer, `effects_buttons` is the entire row.
@ -80,8 +87,9 @@ VideoConferenceTrayEffectsManager::GetSetValueEffects() {
EffectDataVector effects;
for (auto* delegate : effect_delegates_) {
for (auto* effect : delegate->GetEffects(VcEffectType::kSetValue))
for (auto* effect : delegate->GetEffects(VcEffectType::kSetValue)) {
effects.push_back(effect);
}
}
return effects;
@ -92,8 +100,9 @@ VideoConferenceTrayEffectsManager::GetTotalToggleEffectButtons() {
EffectDataVector effects;
for (auto* delegate : effect_delegates_) {
for (auto* effect : delegate->GetEffects(VcEffectType::kToggle))
for (auto* effect : delegate->GetEffects(VcEffectType::kToggle)) {
effects.push_back(effect);
}
}
return effects;

@ -32,6 +32,9 @@ class ASH_EXPORT VideoConferenceTrayEffectsManager {
void RegisterDelegate(VcEffectsDelegate* delegate);
void UnregisterDelegate(VcEffectsDelegate* delegate);
// Returns 'true' if `delegate` is registered, 'false' otherwise.
bool IsDelegateRegistered(VcEffectsDelegate* delegate);
// A vector (or row) of `VcHostedEffect` objects of type
// `VcEffectType::kToggle`.
using EffectDataVector = std::vector<const VcHostedEffect*>;