chromium/ash/curtain/security_curtain_controller_impl_unittest.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/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/curtain/security_curtain_controller.h"
#include "ash/curtain/security_curtain_widget_controller.h"
#include "ash/display/cursor_window_controller.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/system/power/power_button_controller_test_api.h"
#include "ash/system/power/power_button_menu_view.h"
#include "ash/system/power/power_button_test_base.h"
#include "ash/system/privacy_hub/camera_privacy_switch_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/check_deref.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/manager/display_manager.h"
#include "ui/ozone/public/input_controller.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/native_widget.h"
#include "ui/views/widget/widget.h"

namespace aura {
// This improves the error output for our tests that compare `OcclusionState`.
std::ostream& operator<<(std::ostream& os, Window::OcclusionState state) {
  os << Window::OcclusionStateToString(state);
  return os;
}
}  // namespace aura

namespace ash::curtain {
namespace {

using ::testing::Eq;
using ::testing::Ne;

using DisplayId = uint64_t;

ViewFactory FakeViewFactory() {
  return base::BindRepeating(
      []() { return views::Builder<views::View>().Build(); });
}

}  // namespace

class SecurityCurtainControllerImplTest : public PowerButtonTestBase {
 public:
  SecurityCurtainControllerImplTest()
      : PowerButtonTestBase(
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  SecurityCurtainControllerImplTest(const SecurityCurtainControllerImplTest&) =
      delete;
  SecurityCurtainControllerImplTest& operator=(
      const SecurityCurtainControllerImplTest&) = delete;
  ~SecurityCurtainControllerImplTest() override = default;

  void SetUp() override {
    PowerButtonTestBase::SetUp();
    InitPowerButtonControllerMembers(
        chromeos::PowerManagerClient::TabletMode::UNSUPPORTED);
    power_button_test_api_ = std::make_unique<PowerButtonControllerTestApi>(
        Shell::Get()->power_button_controller());
  }

  void TearDown() override {
    power_button_test_api_.reset();
    ResetPowerButtonController();
    PowerButtonTestBase::TearDown();
  }

  SecurityCurtainController& security_curtain_controller() {
    return ash::Shell::Get()->security_curtain_controller();
  }

  CameraPrivacySwitchController& camera_controller() {
    return CHECK_DEREF(CameraPrivacySwitchController::Get());
  }

  PowerButtonControllerTestApi& power_button_test_api() {
    return *power_button_test_api_;
  }

  const ui::InputController& input_controller() {
    return CHECK_DEREF(ui::OzonePlatform::GetInstance()->GetInputController());
  }

  SecurityCurtainController::InitParams init_params() {
    return SecurityCurtainController::InitParams{FakeViewFactory()};
  }

  bool IsNativeCursorEnabled() {
    return !Shell::Get()
                ->window_tree_host_manager()
                ->cursor_window_controller()
                ->is_cursor_compositing_enabled();
  }

  SecurityCurtainController::InitParams WithViewFactory(ViewFactory factory) {
    return SecurityCurtainController::InitParams{factory};
  }

  bool IsCurtainShownOnDisplay(const display::Display& display) {
    return IsCurtainShownOnDisplay(display.id());
  }

  bool IsCurtainShownOnDisplay(DisplayId display_id) {
    auto* root_window_controller =
        Shell::GetRootWindowControllerWithDisplayId(display_id);
    if (!root_window_controller) {
      return false;
    }

    const auto* controller =
        root_window_controller->security_curtain_widget_controller();

    return controller != nullptr;
  }

  views::Widget& GetCurtainForDisplay(const display::Display& display) {
    auto* controller = Shell::GetRootWindowControllerWithDisplayId(display.id())
                           ->security_curtain_widget_controller();
    EXPECT_THAT(controller, testing::NotNull())
        << "Missing curtain widget for display " << display.ToString();

    return controller->GetWidget();
  }

  display::Displays GetDisplays() {
    return display_manager()->active_display_list();
  }

  display::Display GetFirstDisplay() {
    DCHECK(!GetDisplays().empty());
    return GetDisplays().front();
  }

  display::Display CreateSingleDisplay() {
    UpdateDisplay("1111x111");
    return GetFirstDisplay();
  }
  void CreateMultipleDisplays() { UpdateDisplay("1111x111,2222x222,3333x333"); }

  void ResizeDisplay(const display::Display& display,
                     gfx::Size new_resolution) {
    // display::DisplayManagerTestApi offers a `SetDisplayResolution()` method,
    // but that does not seem to work (it does not relay the new resolution to
    // root window associated with the display).
    // So instead of using that method, we simply call `UpdateDisplay`.
    CHECK_EQ(GetDisplays().size(), 1u)
        << "This method only support single display setups!";

    UpdateDisplay(new_resolution.ToString());

    // Sanity check to ensure UpdateDisplay() didn't remote the existing display
    CHECK_EQ(GetFirstDisplay().id(), display.id());
  }

  DisplayId RemoveAllButFirstDisplay() {
    CHECK_GT(GetDisplays().size(), 1u)
        << "This method only works in multi display setups!";

    // UpdateDisplay() will reuse the existing display ids, so by calling it
    // with only a single display, all but the first display will be deleted.
    DisplayId last_display_id = GetDisplays().back().id();
    UpdateDisplay("1111x111");

    CHECK(!display_manager()->IsActiveDisplayId(last_display_id));
    return last_display_id;
  }

  bool IsAudioOutputMuted() {
    return CrasAudioHandler::Get()->IsOutputMutedBySecurityCurtain() &&
           CrasAudioHandler::Get()->IsOutputMuted();
  }

  bool IsAudioInputMuted() {
    return CrasAudioHandler::Get()->IsInputMutedBySecurityCurtain() &&
           CrasAudioHandler::Get()->IsInputMuted();
  }

  views::Widget& GetCurtainWidget() {
    return Shell::GetPrimaryRootWindowController()
        ->security_curtain_widget_controller()
        ->GetWidget();
  }

  views::Widget& GetOpenPowerWidget() {
    EXPECT_TRUE(power_button_test_api().IsMenuOpened());
    return *power_button_test_api().GetPowerButtonMenuView()->GetWidget();
  }

  const aura::Window& GetPowerMenuWidgetContainerParent() {
    EXPECT_TRUE(power_button_test_api().IsMenuOpened());

    return CHECK_DEREF(Shell::GetPrimaryRootWindow()
                           ->GetChildById(kShellWindowId_PowerMenuContainer)
                           ->parent());
  }

 private:
  std::unique_ptr<PowerButtonControllerTestApi> power_button_test_api_;
};

TEST_F(SecurityCurtainControllerImplTest,
       ShouldNotBeEnabledBeforeEnableIsCalled) {
  EXPECT_THAT(security_curtain_controller().IsEnabled(), Eq(false));
}

TEST_F(SecurityCurtainControllerImplTest,
       NoCurtainsShouldBeCreatedBeforeEnableIsCalled) {
  CreateMultipleDisplays();

  for (auto display : GetDisplays()) {
    SCOPED_TRACE("Failure on display " + display.ToString());

    EXPECT_THAT(IsCurtainShownOnDisplay(display), Eq(false));
  }
}

TEST_F(SecurityCurtainControllerImplTest, ShouldBeEnabledWhenCallingEnable) {
  security_curtain_controller().Enable(init_params());

  EXPECT_THAT(security_curtain_controller().IsEnabled(), Eq(true));
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldNotBeEnabledWhenCallingDisable) {
  security_curtain_controller().Enable(init_params());
  security_curtain_controller().Disable();

  EXPECT_THAT(security_curtain_controller().IsEnabled(), Eq(false));
}

TEST_F(SecurityCurtainControllerImplTest,
       CurtainsShouldBeCreatedWhenCallingEnable) {
  CreateMultipleDisplays();

  security_curtain_controller().Enable(init_params());

  for (auto display : GetDisplays()) {
    SCOPED_TRACE("Failure on display " + display.ToString());

    EXPECT_THAT(IsCurtainShownOnDisplay(display), Eq(true));
  }
}

TEST_F(SecurityCurtainControllerImplTest,
       CurtainsShouldBeDestroyedWhenCallingDisable) {
  CreateMultipleDisplays();

  security_curtain_controller().Enable(init_params());
  security_curtain_controller().Disable();

  for (auto display : GetDisplays()) {
    SCOPED_TRACE("Failure on display " + display.ToString());

    EXPECT_THAT(IsCurtainShownOnDisplay(display), Eq(false));
  }
}

TEST_F(SecurityCurtainControllerImplTest, CurtainsShouldCoverTheEntireDisplay) {
  CreateMultipleDisplays();
  security_curtain_controller().Enable(init_params());

  for (auto display : GetDisplays()) {
    SCOPED_TRACE("Failure on display " + display.ToString());
    const views::Widget& curtain = GetCurtainForDisplay(display);
    EXPECT_THAT(curtain.IsVisible(), Eq(true));
    EXPECT_THAT(curtain.GetWindowBoundsInScreen(), Eq(display.bounds()));
  }
}

TEST_F(SecurityCurtainControllerImplTest,
       CurtainShouldKeepCoveringTheEntireDisplayAfterResizing) {
  display::Display display = CreateSingleDisplay();

  security_curtain_controller().Enable(init_params());

  const views::Widget& curtain = GetCurtainForDisplay(display);

  for (gfx::Size new_resolution :
       {gfx::Size(1000, 500), gfx::Size(2000, 1000)}) {
    ResizeDisplay(display, new_resolution);
    EXPECT_THAT(curtain.GetWindowBoundsInScreen().size(), Eq(new_resolution));
  }
}

TEST_F(SecurityCurtainControllerImplTest, CurtainShouldUseViewFactory) {
  // To test that the view factory is used we simply create views with a very
  // specific id, and check that the curtain views have this id.
  constexpr int kId = 1234567;

  CreateMultipleDisplays();

  security_curtain_controller().Enable(WithViewFactory(base::BindRepeating(
      []() { return views::Builder<views::View>().SetID(kId).Build(); })));

  for (auto display : GetDisplays()) {
    views::Widget& curtain = GetCurtainForDisplay(display);

    EXPECT_EQ(curtain.GetContentsView()->GetID(), kId);
  }
}

TEST_F(SecurityCurtainControllerImplTest,
       UncurtainedContainerShouldKeepCoveringTheEntireDisplayAfterResizing) {
  // This test ensures the uncurtained container also has the correct size.
  // That's very important for Chrome Remote Desktop as it streams this
  // container, and if it has the wrong size the stream will fail.
  display::Display display = CreateSingleDisplay();

  security_curtain_controller().Enable(init_params());

  const aura::Window* curtained_off_container = Shell::GetContainer(
      Shell::GetRootWindowForDisplayId(GetFirstDisplay().id()),
      kShellWindowId_ScreenAnimationContainer);

  EXPECT_THAT(curtained_off_container->bounds().size(),
              Eq(display.bounds().size()));

  for (gfx::Size new_resolution :
       {gfx::Size(1000, 500), gfx::Size(2000, 1000)}) {
    ResizeDisplay(display, new_resolution);
    EXPECT_THAT(curtained_off_container->bounds().size(), Eq(new_resolution));
  }
}

TEST_F(SecurityCurtainControllerImplTest, CurtainShouldDisableInputDevices) {
  ASSERT_TRUE(input_controller().AreInputDevicesEnabled());

  auto params = init_params();
  params.disable_input_devices = true;
  security_curtain_controller().Enable(params);

  EXPECT_FALSE(input_controller().AreInputDevicesEnabled());
}

TEST_F(SecurityCurtainControllerImplTest, UncurtainShouldReenableInputDevices) {
  auto params = init_params();
  params.disable_input_devices = true;
  security_curtain_controller().Enable(params);
  security_curtain_controller().Disable();

  EXPECT_TRUE(input_controller().AreInputDevicesEnabled());
}

TEST_F(SecurityCurtainControllerImplTest,
       CurtainShouldNotDisableInputDevicesWhenRequested) {
  auto params = init_params();
  params.disable_input_devices = false;
  security_curtain_controller().Enable(params);

  EXPECT_TRUE(input_controller().AreInputDevicesEnabled());
}

TEST_F(SecurityCurtainControllerImplTest, CurtainShouldNotOccludeOtherWindows) {
  auto other_window = CreateTestWindow(gfx::Rect(100, 100));
  other_window->TrackOcclusionState();
  ASSERT_THAT(other_window->GetOcclusionState(),
              Eq(aura::Window::OcclusionState::VISIBLE));

  security_curtain_controller().Enable(init_params());

  EXPECT_THAT(other_window->GetOcclusionState(),
              Ne(aura::Window::OcclusionState::OCCLUDED));
}

TEST_F(SecurityCurtainControllerImplTest, CurtainShouldNotStealFocus) {
  auto other_window = CreateTestWindow(gfx::Rect(100, 100));
  other_window->Focus();
  ASSERT_THAT(other_window->HasFocus(), Eq(true));

  security_curtain_controller().Enable(init_params());

  EXPECT_THAT(other_window->HasFocus(), Eq(true));
}

TEST_F(SecurityCurtainControllerImplTest, ShouldAddCurtainToNewDisplays) {
  CreateSingleDisplay();

  security_curtain_controller().Enable(init_params());

  CreateMultipleDisplays();
  for (auto display : GetDisplays()) {
    SCOPED_TRACE("Failure on display " + display.ToString());

    EXPECT_THAT(IsCurtainShownOnDisplay(display), Eq(true));
  }
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldRemoveCurtainFromRemovedDisplays) {
  CreateMultipleDisplays();

  security_curtain_controller().Enable(init_params());
  DisplayId removed_display_id = RemoveAllButFirstDisplay();

  EXPECT_THAT(IsCurtainShownOnDisplay(removed_display_id), Eq(false));
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldNotAddCurtainToNewDisplaysAfterCallingDisable) {
  CreateSingleDisplay();

  security_curtain_controller().Enable(init_params());
  security_curtain_controller().Disable();

  CreateMultipleDisplays();
  for (auto display : GetDisplays()) {
    SCOPED_TRACE("Failure on display " + display.ToString());

    EXPECT_THAT(IsCurtainShownOnDisplay(display), Eq(false));
  }
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldMuteAudioOutputAfterRequestedDelayWhileCurtainIsEnabled) {
  CreateSingleDisplay();

  auto delay = base::Minutes(5);
  auto params = init_params();
  params.mute_audio_output_after = delay;
  security_curtain_controller().Enable(params);
  EXPECT_FALSE(IsAudioOutputMuted());

  task_environment()->FastForwardBy(delay - base::Seconds(10));
  EXPECT_FALSE(IsAudioOutputMuted());

  task_environment()->FastForwardBy(base::Seconds(10));
  EXPECT_TRUE(IsAudioOutputMuted());

  security_curtain_controller().Disable();
  EXPECT_FALSE(IsAudioOutputMuted());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldMuteAudioOutputWhileCurtainIsEnabled) {
  CreateSingleDisplay();

  auto params = init_params();
  params.mute_audio_output_after = base::TimeDelta();
  security_curtain_controller().Enable(params);
  task_environment()->RunUntilIdle();  // Audio is muted asynchronously.
  EXPECT_TRUE(IsAudioOutputMuted());

  security_curtain_controller().Disable();
  EXPECT_FALSE(IsAudioOutputMuted());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldNotMuteAudioOutputWhenItsNotRequested) {
  CreateSingleDisplay();

  auto params = init_params();
  params.mute_audio_output_after = base::TimeDelta::Max();
  security_curtain_controller().Enable(params);
  EXPECT_FALSE(IsAudioOutputMuted());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldMuteAudioInputMuteWhileCurtainIsEnabled) {
  CreateSingleDisplay();

  auto params = init_params();
  params.mute_audio_input = true;
  security_curtain_controller().Enable(params);
  EXPECT_TRUE(IsAudioInputMuted());

  security_curtain_controller().Disable();
  EXPECT_FALSE(IsAudioInputMuted());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldNotMuteAudioInputMuteWhenItsNotRequested) {
  CreateSingleDisplay();

  auto params = init_params();
  params.mute_audio_input = false;
  security_curtain_controller().Enable(params);
  EXPECT_FALSE(IsAudioInputMuted());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldDisableNativeMouseCursorWhenEnableIsCalled) {
  ASSERT_THAT(IsNativeCursorEnabled(), Eq(true));

  security_curtain_controller().Enable(init_params());

  ASSERT_THAT(IsNativeCursorEnabled(), Eq(false));
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldReenableNativeMouseCursorWhenDisableIsCalled) {
  security_curtain_controller().Enable(init_params());
  security_curtain_controller().Disable();

  ASSERT_THAT(IsNativeCursorEnabled(), Eq(true));
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldMovePowerMenuWidgetAboveSecurityCurtainWhenEnabled) {
  security_curtain_controller().Enable(init_params());
  PressPowerButton();

  views::Widget& curtain_widget = GetCurtainWidget();
  views::Widget& power_menu_widget = GetOpenPowerWidget();

  ASSERT_TRUE(power_button_test_api().IsMenuOpened());
  EXPECT_TRUE(views::test::WidgetTest::IsWindowStackedAbove(&power_menu_widget,
                                                            &curtain_widget));
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldResetParentOfPowerMenuWidgetWhenDisabled) {
  PressPowerButton();
  const aura::Window& parent_before_enabled =
      GetPowerMenuWidgetContainerParent();
  ReleasePowerButton();

  security_curtain_controller().Enable(init_params());
  security_curtain_controller().Disable();
  PressPowerButton();
  const aura::Window& parent_after_disabled =
      GetPowerMenuWidgetContainerParent();

  ASSERT_EQ(&parent_before_enabled, &parent_after_disabled);
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldDismissOpenPowerMenuWidgetWhenCurtainModeIsEnabled) {
  PressPowerButton();

  security_curtain_controller().Enable(init_params());

  EXPECT_FALSE(power_button_test_api().IsMenuOpened());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldDismissOpenPowerMenuWidgetWhenCurtainModeIsDisabled) {
  security_curtain_controller().Enable(init_params());
  PressPowerButton();
  security_curtain_controller().Disable();

  EXPECT_FALSE(power_button_test_api().IsMenuOpened());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldForceDisableCameraAccessWhenCurtainModeIsEnabled) {
  auto params = init_params();
  params.disable_camera_access = true;
  security_curtain_controller().Enable(params);
  EXPECT_TRUE(camera_controller().IsCameraAccessForceDisabled());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldStopForceDisablingCameraAccessWhenCurtainModeIsDisabled) {
  auto params = init_params();
  params.disable_camera_access = true;
  security_curtain_controller().Enable(params);
  security_curtain_controller().Disable();

  EXPECT_FALSE(camera_controller().IsCameraAccessForceDisabled());
}

TEST_F(SecurityCurtainControllerImplTest,
       ShouldNotForceDisableCameraAccessWhenNotRequested) {
  auto params = init_params();
  params.disable_camera_access = false;
  security_curtain_controller().Enable(params);
  EXPECT_FALSE(camera_controller().IsCameraAccessForceDisabled());
}

}  // namespace ash::curtain