| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/exo/wayland/zaura_shell.h" |
| |
| #include <aura-shell-server-protocol.h> |
| |
| #include <memory> |
| |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/wm/desks/desks_util.h" |
| #include "ash/wm/window_util.h" |
| #include "base/time/time.h" |
| #include "components/exo/buffer.h" |
| #include "components/exo/test/exo_test_base.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/aura/window_occlusion_tracker.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animation_observer.h" |
| #include "ui/compositor/layer_animation_sequence.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/compositor/test/layer_animator_test_controller.h" |
| #include "ui/gfx/geometry/size_f.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/window_util.h" |
| #include "ui/wm/public/activation_change_observer.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| namespace exo { |
| namespace wayland { |
| |
| namespace { |
| |
| constexpr auto kTransitionDuration = base::Seconds(3); |
| |
| class TestAuraSurface : public AuraSurface { |
| public: |
| explicit TestAuraSurface(Surface* surface) |
| : AuraSurface(surface, /*resource=*/nullptr) {} |
| |
| TestAuraSurface(const TestAuraSurface&) = delete; |
| TestAuraSurface& operator=(const TestAuraSurface&) = delete; |
| |
| float last_sent_occlusion_fraction() const { |
| return last_sent_occlusion_fraction_; |
| } |
| aura::Window::OcclusionState last_sent_occlusion_state() const { |
| return last_sent_occlusion_state_; |
| } |
| int num_occlusion_updates() const { return num_occlusion_updates_; } |
| |
| protected: |
| void SendOcclusionFraction(float occlusion_fraction) override { |
| last_sent_occlusion_fraction_ = occlusion_fraction; |
| num_occlusion_updates_++; |
| } |
| |
| void SendOcclusionState( |
| const aura::Window::OcclusionState occlusion_state) override { |
| last_sent_occlusion_state_ = occlusion_state; |
| } |
| |
| private: |
| float last_sent_occlusion_fraction_ = -1.0f; |
| aura::Window::OcclusionState last_sent_occlusion_state_ = |
| aura::Window::OcclusionState::UNKNOWN; |
| int num_occlusion_updates_ = 0; |
| }; |
| |
| class MockSurfaceDelegate : public SurfaceDelegate { |
| public: |
| MOCK_METHOD(void, OnSurfaceCommit, (), (override)); |
| MOCK_METHOD(bool, IsSurfaceSynchronized, (), (const, override)); |
| MOCK_METHOD(bool, IsInputEnabled, (Surface * surface), (const, override)); |
| MOCK_METHOD(void, OnSetFrame, (SurfaceFrameType type), (override)); |
| MOCK_METHOD(void, |
| OnSetFrameColors, |
| (SkColor active_color, SkColor inactive_color), |
| (override)); |
| MOCK_METHOD(void, |
| OnSetParent, |
| (Surface * parent, const gfx::Point& position), |
| (override)); |
| MOCK_METHOD(void, OnSetStartupId, (const char* startup_id), (override)); |
| MOCK_METHOD(void, |
| OnSetApplicationId, |
| (const char* application_id), |
| (override)); |
| MOCK_METHOD(void, SetUseImmersiveForFullscreen, (bool value), (override)); |
| MOCK_METHOD(void, OnActivationRequested, (), (override)); |
| MOCK_METHOD(void, OnNewOutputAdded, (), (override)); |
| MOCK_METHOD(void, OnSetServerStartResize, (), (override)); |
| MOCK_METHOD(void, ShowSnapPreviewToPrimary, (), (override)); |
| MOCK_METHOD(void, ShowSnapPreviewToSecondary, (), (override)); |
| MOCK_METHOD(void, HideSnapPreview, (), (override)); |
| MOCK_METHOD(void, SetSnappedToSecondary, (), (override)); |
| MOCK_METHOD(void, SetSnappedToPrimary, (), (override)); |
| MOCK_METHOD(void, UnsetSnap, (), (override)); |
| MOCK_METHOD(void, SetCanGoBack, (), (override)); |
| MOCK_METHOD(void, UnsetCanGoBack, (), (override)); |
| MOCK_METHOD(void, SetPip, (), (override)); |
| MOCK_METHOD(void, UnsetPip, (), (override)); |
| MOCK_METHOD(void, |
| SetAspectRatio, |
| (const gfx::SizeF& aspect_ratio), |
| (override)); |
| MOCK_METHOD(void, MoveToDesk, (int desk_index), (override)); |
| MOCK_METHOD(void, SetVisibleOnAllWorkspaces, (), (override)); |
| MOCK_METHOD(void, |
| SetInitialWorkspace, |
| (const char* initial_workspace), |
| (override)); |
| MOCK_METHOD(void, Pin, (bool trusted), (override)); |
| MOCK_METHOD(void, Unpin, (), (override)); |
| MOCK_METHOD(void, SetSystemModal, (bool modal), (override)); |
| }; |
| |
| } // namespace |
| |
| class ZAuraSurfaceTest : public test::ExoTestBase, |
| public ::wm::ActivationChangeObserver { |
| public: |
| ZAuraSurfaceTest() {} |
| |
| ZAuraSurfaceTest(const ZAuraSurfaceTest&) = delete; |
| ZAuraSurfaceTest& operator=(const ZAuraSurfaceTest&) = delete; |
| |
| ~ZAuraSurfaceTest() override {} |
| |
| // test::ExoTestBase overrides: |
| void SetUp() override { |
| test::ExoTestBase::SetUp(); |
| |
| gfx::Size buffer_size(10, 10); |
| std::unique_ptr<Buffer> buffer( |
| new Buffer(exo_test_helper()->CreateGpuMemoryBuffer(buffer_size))); |
| |
| surface_ = std::make_unique<Surface>(); |
| surface_->Attach(buffer.get()); |
| |
| aura_surface_ = std::make_unique<TestAuraSurface>(surface_.get()); |
| |
| gfx::Transform transform; |
| transform.Scale(1.5f, 1.5f); |
| parent_widget_ = CreateTestWidget(); |
| parent_widget_->SetBounds(gfx::Rect(0, 0, 10, 10)); |
| parent_widget_->GetNativeWindow()->SetTransform(transform); |
| parent_widget_->GetNativeWindow()->AddChild(surface_->window()); |
| parent_widget_->Show(); |
| surface_->window()->SetBounds(gfx::Rect(5, 5, 10, 10)); |
| surface_->window()->Show(); |
| |
| ash::Shell::Get()->activation_client()->AddObserver(this); |
| aura_surface_->SetOcclusionTracking(true); |
| } |
| |
| void TearDown() override { |
| ash::Shell::Get()->activation_client()->RemoveObserver(this); |
| |
| parent_widget_.reset(); |
| aura_surface_.reset(); |
| surface_.reset(); |
| |
| test::ExoTestBase::TearDown(); |
| } |
| |
| // ::wm::ActivationChangeObserver overrides: |
| void OnWindowActivated(ActivationReason reason, |
| aura::Window* gained_active, |
| aura::Window* lost_active) override { |
| if (lost_active == parent_widget_->GetNativeWindow()) { |
| occlusion_fraction_on_activation_loss_ = |
| aura_surface().last_sent_occlusion_fraction(); |
| } |
| } |
| |
| protected: |
| TestAuraSurface& aura_surface() { return *aura_surface_; } |
| Surface& surface() { return *surface_; } |
| views::Widget& parent_widget() { return *parent_widget_; } |
| float occlusion_fraction_on_activation_loss() const { |
| return occlusion_fraction_on_activation_loss_; |
| } |
| |
| std::unique_ptr<views::Widget> CreateOpaqueWidget(const gfx::Rect& bounds) { |
| return CreateTestWidget( |
| /*delegate=*/nullptr, |
| /*container_id=*/ash::desks_util::GetActiveDeskContainerId(), bounds, |
| /*show=*/false); |
| } |
| |
| private: |
| std::unique_ptr<TestAuraSurface> aura_surface_; |
| std::unique_ptr<Surface> surface_; |
| std::unique_ptr<views::Widget> parent_widget_; |
| float occlusion_fraction_on_activation_loss_ = -1.0f; |
| }; |
| |
| TEST_F(ZAuraSurfaceTest, OcclusionTrackingStartsAfterCommit) { |
| surface().OnWindowOcclusionChanged(); |
| |
| EXPECT_EQ(-1.0f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::UNKNOWN, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(0, aura_surface().num_occlusion_updates()); |
| EXPECT_FALSE(surface().IsTrackingOcclusion()); |
| |
| auto widget = CreateOpaqueWidget(gfx::Rect(0, 0, 10, 10)); |
| widget->Show(); |
| surface().Commit(); |
| |
| EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(1, aura_surface().num_occlusion_updates()); |
| EXPECT_TRUE(surface().IsTrackingOcclusion()); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, |
| LosingActivationWithNoAnimatingWindowsSendsCorrectOcclusionFraction) { |
| surface().Commit(); |
| EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(1, aura_surface().num_occlusion_updates()); |
| ::wm::ActivateWindow(parent_widget().GetNativeWindow()); |
| |
| // Creating an opaque window but don't show it. |
| auto widget = CreateOpaqueWidget(gfx::Rect(0, 0, 10, 10)); |
| |
| // Occlusion sent before de-activation should include that widget. |
| widget->Show(); |
| EXPECT_EQ(0.2f, occlusion_fraction_on_activation_loss()); |
| EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(2, aura_surface().num_occlusion_updates()); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, |
| LosingActivationWithAnimatingWindowsSendsTargetOcclusionFraction) { |
| surface().Commit(); |
| EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(1, aura_surface().num_occlusion_updates()); |
| ::wm::ActivateWindow(parent_widget().GetNativeWindow()); |
| |
| // Creating an opaque window but don't show it. |
| auto widget = CreateOpaqueWidget(gfx::Rect(0, 0, 10, 10)); |
| widget->GetNativeWindow()->layer()->SetOpacity(0.0f); |
| |
| ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode( |
| ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); |
| ui::LayerAnimatorTestController test_controller( |
| ui::LayerAnimator::CreateImplicitAnimator()); |
| ui::ScopedLayerAnimationSettings layer_animation_settings( |
| test_controller.animator()); |
| layer_animation_settings.SetTransitionDuration(kTransitionDuration); |
| widget->GetNativeWindow()->layer()->SetAnimator(test_controller.animator()); |
| widget->GetNativeWindow()->layer()->SetOpacity(1.0f); |
| |
| // Opacity animation uses threaded animation. |
| test_controller.StartThreadedAnimationsIfNeeded(); |
| test_controller.Step(kTransitionDuration / 3); |
| |
| // No occlusion updates should happen until the window is de-activated. |
| EXPECT_EQ(1, aura_surface().num_occlusion_updates()); |
| |
| // Occlusion sent before de-activation should include the window animating |
| // to be completely opaque. |
| widget->Show(); |
| EXPECT_EQ(0.2f, occlusion_fraction_on_activation_loss()); |
| EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(2, aura_surface().num_occlusion_updates()); |
| |
| // Explicitly stop animation because threaded animation may have started |
| // a bit later. |kTransitionDuration| may not be quite enough to reach the |
| // end. |
| test_controller.Step(kTransitionDuration / 3); |
| test_controller.Step(kTransitionDuration / 3); |
| widget->GetNativeWindow()->layer()->GetAnimator()->StopAnimating(); |
| widget->GetNativeWindow()->layer()->SetAnimator(nullptr); |
| |
| // Expect the occlusion tracker to send an update after the animation |
| // finishes. |
| EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(3, aura_surface().num_occlusion_updates()); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, |
| LosingActivationByTriggeringTheLockScreenDoesNotSendOccludedFraction) { |
| surface().Commit(); |
| EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| EXPECT_EQ(1, aura_surface().num_occlusion_updates()); |
| ::wm::ActivateWindow(parent_widget().GetNativeWindow()); |
| |
| // Lock the screen. |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW); |
| auto lock_widget = std::make_unique<views::Widget>(); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.context = GetContext(); |
| params.bounds = gfx::Rect(0, 0, 100, 100); |
| lock_widget->Init(std::move(params)); |
| ash::Shell::GetContainer(ash::Shell::GetPrimaryRootWindow(), |
| ash::kShellWindowId_LockScreenContainer) |
| ->AddChild(lock_widget->GetNativeView()); |
| |
| // Simulate real screen locker to change session state to LOCKED |
| // when it is shown. |
| auto* controller = ash::Shell::Get()->session_controller(); |
| GetSessionControllerClient()->LockScreen(); |
| lock_widget->Show(); |
| EXPECT_TRUE(controller->IsScreenLocked()); |
| EXPECT_TRUE(lock_widget->GetNativeView()->HasFocus()); |
| |
| // We should have lost focus, but not reported that the window has been |
| // fully occluded. |
| EXPECT_NE(parent_widget().GetNativeWindow(), |
| ash::window_util::GetActiveWindow()); |
| EXPECT_EQ(0.0f, occlusion_fraction_on_activation_loss()); |
| EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, OcclusionIncludesOffScreenArea) { |
| UpdateDisplay("200x150"); |
| |
| gfx::Size buffer_size(80, 100); |
| std::unique_ptr<Buffer> buffer( |
| new Buffer(exo_test_helper()->CreateGpuMemoryBuffer(buffer_size))); |
| // This is scaled by 1.5 - set the bounds to (-60, 75, 120, 150) in screen |
| // coordinates so 75% of it is outside of the 100x100 screen. |
| surface().window()->SetBounds(gfx::Rect(-40, 50, 80, 100)); |
| surface().Attach(buffer.get()); |
| surface().Commit(); |
| |
| surface().OnWindowOcclusionChanged(); |
| |
| EXPECT_EQ(0.75f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, ZeroSizeWindowSendsZeroOcclusionFraction) { |
| // Zero sized window should not be occluded. |
| surface().window()->SetBounds(gfx::Rect(0, 0, 0, 0)); |
| surface().Commit(); |
| surface().OnWindowOcclusionChanged(); |
| EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction()); |
| EXPECT_EQ(aura::Window::OcclusionState::VISIBLE, |
| aura_surface().last_sent_occlusion_state()); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, CanSetFullscreenModeToPlain) { |
| MockSurfaceDelegate delegate; |
| wl_resource resource; |
| resource.data = &aura_surface(); |
| surface().SetSurfaceDelegate(&delegate); |
| EXPECT_CALL(delegate, SetUseImmersiveForFullscreen(false)); |
| |
| aura_surface().SetFullscreenMode(ZAURA_SURFACE_FULLSCREEN_MODE_PLAIN); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, CanPin) { |
| MockSurfaceDelegate delegate; |
| wl_resource resource; |
| resource.data = &aura_surface(); |
| surface().SetSurfaceDelegate(&delegate); |
| EXPECT_CALL(delegate, Pin(true)); |
| |
| aura_surface().Pin(true); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, CanUnpin) { |
| MockSurfaceDelegate delegate; |
| wl_resource resource; |
| resource.data = &aura_surface(); |
| surface().SetSurfaceDelegate(&delegate); |
| EXPECT_CALL(delegate, Unpin()); |
| |
| aura_surface().Unpin(); |
| } |
| |
| TEST_F(ZAuraSurfaceTest, CanSetFullscreenModeToImmersive) { |
| MockSurfaceDelegate delegate; |
| surface().SetSurfaceDelegate(&delegate); |
| EXPECT_CALL(delegate, SetUseImmersiveForFullscreen(true)); |
| |
| aura_surface().SetFullscreenMode(ZAURA_SURFACE_FULLSCREEN_MODE_IMMERSIVE); |
| } |
| |
| } // namespace wayland |
| } // namespace exo |