chromium/ash/color_enhancement/color_enhancement_controller_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/color_enhancement/color_enhancement_controller.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/flash_screen_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/cursor_window_controller.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "components/prefs/pref_service.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"

namespace ash {

namespace {
const int kDelayToCheckStateChangeMs = 300;
const int kBlueColor = 0x0000ff;

// Indices in a FilterOperation::Matrix that map a channel to itself.
const int kRedToRedIndex = 0;
const int kGreenToGreenIndex = 6;
const int kBlueToBlueIndex = 12;
const int kAlphaToAlphaIndex = 18;
}  // namespace

// Tests that the color enhancement controller sets the appropriate values
// on the root window layer and updates cursor compositing as needed.
class ColorEnhancementControllerTest : public AshTestBase {
 public:
  ColorEnhancementControllerTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  ColorEnhancementControllerTest(const ColorEnhancementControllerTest&) =
      delete;
  ColorEnhancementControllerTest& operator=(
      const ColorEnhancementControllerTest&) = delete;

  ~ColorEnhancementControllerTest() override = default;

  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(
        ::features::kAccessibilityFlashScreenFeature);
    AshTestBase::SetUp();
  }

  bool IsCursorCompositingEnabled() const {
    return Shell::Get()
        ->window_tree_host_manager()
        ->cursor_window_controller()
        ->is_cursor_compositing_enabled();
  }

  PrefService* GetPrefs() const {
    return Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  }

  void FastForwardAnimationTo(int milliseconds) {
    gfx::AnimationTestApi animation_api(
        AccessibilityController::Get()
            ->GetFlashScreenControllerForTesting()
            ->GetAnimationForTesting());
    base::TimeTicks now = base::TimeTicks::Now();
    animation_api.SetStartTime(now);
    animation_api.Step(now + base::Milliseconds(milliseconds));
  }

  void ShowNotification() {
    const std::string notification_id("id");
    const std::string notification_title("title");
    message_center::MessageCenter::Get()->AddNotification(
        std::make_unique<message_center::Notification>(
            message_center::NOTIFICATION_TYPE_SIMPLE, notification_id,
            base::UTF8ToUTF16(notification_title), u"test message",
            ui::ImageModel(), std::u16string() /* display_source */, GURL(),
            message_center::NotifierId(),
            message_center::RichNotificationData(),
            new message_center::NotificationDelegate()));
  }

  void ExpectNoCustomColorMatrix() {
    for (aura::Window* root_window : Shell::GetAllRootWindows()) {
      const cc::FilterOperation::Matrix* matrix =
          root_window->layer()->GetLayerCustomColorMatrix();
      EXPECT_FALSE(matrix);
    }
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(ColorEnhancementControllerTest, HighContrast) {
  PrefService* prefs = GetPrefs();
  prefs->SetBoolean(prefs::kAccessibilityHighContrastEnabled, true);
  EXPECT_TRUE(IsCursorCompositingEnabled());
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_TRUE(root_window->layer()->layer_inverted());
  }
  prefs->SetBoolean(prefs::kAccessibilityHighContrastEnabled, false);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FALSE(root_window->layer()->layer_inverted());
  }
  EXPECT_FALSE(IsCursorCompositingEnabled());
}

TEST_F(ColorEnhancementControllerTest, Greyscale) {
  PrefService* prefs = GetPrefs();
  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, true);
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 0);
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionType,
                    ColorVisionCorrectionType::kGrayscale);
  EXPECT_FALSE(IsCursorCompositingEnabled());
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.f, root_window->layer()->layer_grayscale());
    // No other color filters were set.
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 100);
  EXPECT_FALSE(IsCursorCompositingEnabled());
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(1, root_window->layer()->layer_grayscale());
    // No other color filters were set.
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 50);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_grayscale());
    // No other color filters were set.
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  // Greyscale larger than 100% or smaller than 0% does nothing.
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 500);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_grayscale());
    // No other color filters were set.
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, -10);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_grayscale());
    // No other color filters were set.
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }
}

TEST_F(ColorEnhancementControllerTest, ColorVisionCorrectionFilters) {
  PrefService* prefs = GetPrefs();
  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, true);

  // Try for each of the color correction types.
  for (int i = 0; i < 3; i++) {
    prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionType, i);

    // With severity at 0, no matrix should be applied.
    prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 0);
    for (aura::Window* root_window : Shell::GetAllRootWindows()) {
      EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
      EXPECT_FLOAT_EQ(0.f, root_window->layer()->layer_grayscale());
    }

    // With a non-zero severity, a matrix should be applied.
    prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 50);
    for (aura::Window* root_window : Shell::GetAllRootWindows()) {
      EXPECT_TRUE(root_window->layer()->LayerHasCustomColorMatrix());
      // Grayscale was not impacted.
      EXPECT_FLOAT_EQ(0.f, root_window->layer()->layer_grayscale());
    }
    prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 100);
    for (aura::Window* root_window : Shell::GetAllRootWindows()) {
      const cc::FilterOperation::Matrix* matrix =
          root_window->layer()->GetLayerCustomColorMatrix();
      EXPECT_TRUE(matrix);
      // For protanopes (i == 1), the first row in the resulting matrix should
      // have a 1 for red and a zero for the other colors. Similarly with
      // deuteranopes (i == 2) and tritanopes (i == 3). This ensures we are
      // correcting around the right axis.
      for (int j = 0; j < 3; j++) {
        if (i == j) {
          EXPECT_EQ(1, matrix->at(i * 5 + j));
        } else {
          EXPECT_EQ(0, matrix->at(i * 5 + j));
        }
      }
    }
  }
}

TEST_F(ColorEnhancementControllerTest, GrayscaleBehindColorCorrectionOption) {
  PrefService* prefs = GetPrefs();
  // Color filtering off.
  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, false);
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 50);
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionType,
                    ColorVisionCorrectionType::kGrayscale);

  // Default values.
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_grayscale());
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  // Turn on color filtering, values should now be from prefs.
  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, true);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_grayscale());
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionType,
                    ColorVisionCorrectionType::kDeuteranomaly);
  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, true);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_grayscale());
    EXPECT_TRUE(root_window->layer()->LayerHasCustomColorMatrix());
  }

  // Turn it off again, expect defaults to be restored.
  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, false);
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_grayscale());
    EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
  }
}

TEST_F(ColorEnhancementControllerTest, FlashNotifications) {
  PrefService* prefs = GetPrefs();
  prefs->SetBoolean(prefs::kAccessibilityFlashNotificationsEnabled, true);

  // Show a normal notification. Flashing should occur.
  ShowNotification();

  // The color should be shown for kDelayToCheckStateChangeMs duration, then be
  // off.
  for (int i = 1; i < kDelayToCheckStateChangeMs; i++) {
    FastForwardAnimationTo(i);
    // A custom color matrix has been shown.
    for (aura::Window* root_window : Shell::GetAllRootWindows()) {
      const cc::FilterOperation::Matrix* matrix =
          root_window->layer()->GetLayerCustomColorMatrix();
      ASSERT_TRUE(matrix);
      // The default flash color is yellow, which has r == g == 1.0, b = 0.0,
      // and alpha = 1.0. b however is not set to 0.0 since we use 30% of the
      // original color.
      EXPECT_EQ((*matrix)[kRedToRedIndex], 1.0f);
      EXPECT_EQ((*matrix)[kGreenToGreenIndex], 1.0f);
      EXPECT_NE((*matrix)[kBlueToBlueIndex], 1.0f);
      EXPECT_NE((*matrix)[kBlueToBlueIndex], 0.0f);
      EXPECT_EQ((*matrix)[kAlphaToAlphaIndex], 1.0f);
    }
  }
  FastForwardAnimationTo(kDelayToCheckStateChangeMs);

  // Off: Completed a throb animation.
  ExpectNoCustomColorMatrix();

  // Note: The throb animation does not play well with the AnimationTestAPI so
  // we can only test the first throb cycle.
}

TEST_F(ColorEnhancementControllerTest, FlashNotificationsColorPref) {
  PrefService* prefs = GetPrefs();
  prefs->SetBoolean(prefs::kAccessibilityFlashNotificationsEnabled, true);

  // Set the color to blue.
  prefs->SetInteger(prefs::kAccessibilityFlashNotificationsColor, 0x0000ff);

  // Show a normal notification. Flashing should occur.
  ShowNotification();
  FastForwardAnimationTo(1);

  // A custom color matrix has been shown.
  for (aura::Window* root_window : Shell::GetAllRootWindows()) {
    const cc::FilterOperation::Matrix* matrix =
        root_window->layer()->GetLayerCustomColorMatrix();
    ASSERT_TRUE(matrix);
    // Blue has r == g !== 1.0, b = 1.0, and alpha = 1.0.
    EXPECT_NE((*matrix)[kRedToRedIndex], 1.0f);
    EXPECT_NE((*matrix)[kGreenToGreenIndex], 1.0f);
    EXPECT_EQ((*matrix)[kRedToRedIndex], (*matrix)[kGreenToGreenIndex]);
    EXPECT_EQ((*matrix)[kBlueToBlueIndex], 1.0f);
    EXPECT_EQ((*matrix)[kAlphaToAlphaIndex], 1.0f);
  }

  FastForwardAnimationTo(kDelayToCheckStateChangeMs);

  // Should be off now.
  ExpectNoCustomColorMatrix();
}

TEST_F(ColorEnhancementControllerTest,
       FlashNotificationsResetsColorCorrectionOnComplete) {
  PrefService* prefs = GetPrefs();
  prefs->SetBoolean(prefs::kAccessibilityFlashNotificationsEnabled, true);
  prefs->SetInteger(prefs::kAccessibilityFlashNotificationsColor, kBlueColor);

  prefs->SetBoolean(prefs::kAccessibilityColorCorrectionEnabled, true);
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 50);
  prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionType,
                    ColorVisionCorrectionType::kDeuteranomaly);

  // Color correction matrix is set.
  aura::Window* root_window = Shell::GetPrimaryRootWindow();
  const cc::FilterOperation::Matrix* color_correction_matrix =
      root_window->layer()->GetLayerCustomColorMatrix();
  ASSERT_TRUE(color_correction_matrix);
  float r = (*color_correction_matrix)[kRedToRedIndex];
  float g = (*color_correction_matrix)[kGreenToGreenIndex];
  float b = (*color_correction_matrix)[kBlueToBlueIndex];

  // Show a normal notification. Flashing should occur.
  ShowNotification();
  FastForwardAnimationTo(1);

  // Overlay color matrix is shown instead.
  const cc::FilterOperation::Matrix* matrix =
      root_window->layer()->GetLayerCustomColorMatrix();
  ASSERT_TRUE(matrix);
  EXPECT_NE(r, (*matrix)[kRedToRedIndex]);
  EXPECT_NE(g, (*matrix)[kGreenToGreenIndex]);
  EXPECT_NE(b, (*matrix)[kBlueToBlueIndex]);

  FastForwardAnimationTo(kDelayToCheckStateChangeMs);
  // Flash is off, color correction matrix is shown again.
  matrix = root_window->layer()->GetLayerCustomColorMatrix();
  EXPECT_EQ(r, (*matrix)[kRedToRedIndex]);
  EXPECT_EQ(g, (*matrix)[kGreenToGreenIndex]);
  EXPECT_EQ(b, (*matrix)[kBlueToBlueIndex]);
}

}  // namespace ash