// Copyright 2023 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/input_device_settings/keyboard_modifier_metrics_recorder.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/ash_prefs.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_helper.h"
#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "ui/events/ash/mojom/modifier_key.mojom.h"
#include "ui/events/ash/pref_names.h"
namespace ash {
namespace {
constexpr char kUserEmail1[] = "[email protected]";
constexpr char kUserEmail2[] = "[email protected]";
// Table containing the list of modifier remapping prefs with their expected
// metric names and their matching default key. Used by the following test
// suites.
struct KeyboardModifierMetricsRecorderTestData {
std::string pref_name;
std::string changed_metric_name;
std::string started_metric_name;
ui::mojom::ModifierKey default_modifier_key;
} kKeyboardModifierMetricTestData[] = {
{prefs::kLanguageRemapAltKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.AltRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.AltRemappedTo.Started",
ui::mojom::ModifierKey::kAlt},
{prefs::kLanguageRemapControlKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.ControlRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.ControlRemappedTo.Started",
ui::mojom::ModifierKey::kControl},
{prefs::kLanguageRemapEscapeKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.EscapeRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.EscapeRemappedTo.Started",
ui::mojom::ModifierKey::kEscape},
{prefs::kLanguageRemapBackspaceKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.BackspaceRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.BackspaceRemappedTo.Started",
ui::mojom::ModifierKey::kBackspace},
{prefs::kLanguageRemapAssistantKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.AssistantRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.AssistantRemappedTo.Started",
ui::mojom::ModifierKey::kAssistant},
{prefs::kLanguageRemapCapsLockKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.CapsLockRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.CapsLockRemappedTo.Started",
ui::mojom::ModifierKey::kCapsLock},
{prefs::kLanguageRemapSearchKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.SearchRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.SearchRemappedTo.Started",
ui::mojom::ModifierKey::kMeta},
{prefs::kLanguageRemapExternalMetaKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.ExternalMetaRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.ExternalMetaRemappedTo.Started",
ui::mojom::ModifierKey::kMeta},
{prefs::kLanguageRemapExternalCommandKeyTo,
"ChromeOS.Settings.Keyboard.Modifiers.ExternalCommandRemappedTo.Changed",
"ChromeOS.Settings.Keyboard.Modifiers.ExternalCommandRemappedTo.Started",
ui::mojom::ModifierKey::kControl},
};
} // namespace
class KeyboardModifierMetricsRecorderTest : public AshTestBase {
public:
KeyboardModifierMetricsRecorderTest() = default;
KeyboardModifierMetricsRecorderTest(
const KeyboardModifierMetricsRecorderTest&) = delete;
KeyboardModifierMetricsRecorderTest& operator=(
const KeyboardModifierMetricsRecorderTest&) = delete;
~KeyboardModifierMetricsRecorderTest() override = default;
void SetUp() override {
feature_list_.InitAndDisableFeature(features::kInputDeviceSettingsSplit);
AshTestBase::SetUp();
ResetHistogramTester();
recorder_ = Shell::Get()->keyboard_modifier_metrics_recorder();
}
void TearDown() override {
histogram_tester_.reset();
AshTestBase::TearDown();
}
void ResetHistogramTester() {
histogram_tester_ = std::make_unique<base::HistogramTester>();
}
protected:
raw_ptr<KeyboardModifierMetricsRecorder, DanglingUntriaged> recorder_;
std::unique_ptr<base::HistogramTester> histogram_tester_;
base::test::ScopedFeatureList feature_list_;
};
class KeyboardModifierMetricsRecorderPrefChangedTest
: public KeyboardModifierMetricsRecorderTest,
public testing::WithParamInterface<
std::tuple<KeyboardModifierMetricsRecorderTestData, int, int>> {
void SetUp() override {
KeyboardModifierMetricsRecorderTest::SetUp();
int int_modifier_key_from, int_modifier_key_to;
std::tie(data_, int_modifier_key_from, int_modifier_key_to) = GetParam();
modifier_key_from_ =
static_cast<ui::mojom::ModifierKey>(int_modifier_key_from);
modifier_key_to_ = static_cast<ui::mojom::ModifierKey>(int_modifier_key_to);
pref_service_ = Shell::Get()->session_controller()->GetActivePrefService();
pref_service_->SetInteger(data_.pref_name,
static_cast<int>(data_.default_modifier_key));
ResetHistogramTester();
}
protected:
raw_ptr<PrefService, DanglingUntriaged> pref_service_;
KeyboardModifierMetricsRecorderTestData data_;
ui::mojom::ModifierKey modifier_key_from_;
ui::mojom::ModifierKey modifier_key_to_;
};
// Instantiates the test case with every combination of the modifiers in
// `kKeyboardModifierMetricTestData` and every combination of both a modifier
// to start with and a modifier to change to. Custom name generator is
// implemented to simplify searching through test results for failed cases.
INSTANTIATE_TEST_SUITE_P(
,
KeyboardModifierMetricsRecorderPrefChangedTest,
testing::Combine(
testing::ValuesIn(kKeyboardModifierMetricTestData),
testing::Range(static_cast<int>(ui::mojom::ModifierKey::kMinValue),
static_cast<int>(ui::mojom::ModifierKey::kAssistant) +
1),
testing::Range(static_cast<int>(ui::mojom::ModifierKey::kMinValue),
static_cast<int>(ui::mojom::ModifierKey::kAssistant) +
1)),
([](const testing::TestParamInfo<
KeyboardModifierMetricsRecorderPrefChangedTest::ParamType>& info) {
const auto& [data, int_modifier_key_from, int_modifier_key_to] =
info.param;
// Pref name must replace periods with underscores for gtest output.
std::string result;
base::ReplaceChars(data.pref_name, ".", "_", &result);
return base::StringPrintf("%s_%d_%d", result.c_str(),
int_modifier_key_from, int_modifier_key_to);
}));
TEST_P(KeyboardModifierMetricsRecorderPrefChangedTest, CheckChangedMetric) {
pref_service_->SetInteger(data_.pref_name,
static_cast<int>(modifier_key_from_));
if (modifier_key_from_ != data_.default_modifier_key) {
histogram_tester_->ExpectUniqueSample(data_.changed_metric_name,
modifier_key_from_, 1);
} else {
histogram_tester_->ExpectUniqueSample(data_.changed_metric_name,
modifier_key_from_, 0);
}
ResetHistogramTester();
pref_service_->SetInteger(data_.pref_name,
static_cast<int>(modifier_key_to_));
if (modifier_key_from_ != modifier_key_to_) {
histogram_tester_->ExpectUniqueSample(data_.changed_metric_name,
modifier_key_to_, 1);
} else {
histogram_tester_->ExpectUniqueSample(data_.changed_metric_name,
modifier_key_to_, 0);
}
}
class KeyboardModifierMetricsRecorderPrefStartedTest
: public KeyboardModifierMetricsRecorderTest,
public testing::WithParamInterface<
std::tuple<KeyboardModifierMetricsRecorderTestData, int>> {
void SetUp() override {
KeyboardModifierMetricsRecorderTest::SetUp();
int int_modifier_key;
std::tie(data_, int_modifier_key) = GetParam();
modifier_key_ = static_cast<ui::mojom::ModifierKey>(int_modifier_key);
ResetHistogramTester();
}
protected:
KeyboardModifierMetricsRecorderTestData data_;
ui::mojom::ModifierKey modifier_key_;
};
// Instantiates the test case with every combination of the modifiers in
// `kKeyboardModifierMetricTestData` and with every possible remapped value in
// `ui::mojom::ModifierKey`. A custom name generator is implemented to simplify
// searching through test results for failed cases.
INSTANTIATE_TEST_SUITE_P(
,
KeyboardModifierMetricsRecorderPrefStartedTest,
testing::Combine(
testing::ValuesIn(kKeyboardModifierMetricTestData),
// Only test from 0 to Assistant as those are the only modifiers
// supported pre settings split.
testing::Range(static_cast<int>(ui::mojom::ModifierKey::kMinValue),
static_cast<int>(ui::mojom::ModifierKey::kAssistant) +
1)),
([](const testing::TestParamInfo<
KeyboardModifierMetricsRecorderPrefStartedTest::ParamType>& info) {
const auto& [data, int_modifier_key] = info.param;
// Pref name must replace periods with underscores for gtest output.
std::string result;
base::ReplaceChars(data.pref_name, ".", "_", &result);
return base::StringPrintf("%s_%d", result.c_str(), int_modifier_key);
}));
TEST_P(KeyboardModifierMetricsRecorderPrefStartedTest, InitializeTest) {
// Initialize two pref services with the initial value of the metric.
const AccountId account_id1 = AccountId::FromUserEmail(kUserEmail1);
const AccountId account_id2 = AccountId::FromUserEmail(kUserEmail2);
std::unique_ptr<TestingPrefServiceSimple> pref_service1 =
std::make_unique<TestingPrefServiceSimple>();
ash::RegisterUserProfilePrefs(pref_service1->registry(), /*country=*/"",
true);
std::unique_ptr<TestingPrefServiceSimple> pref_service2 =
std::make_unique<TestingPrefServiceSimple>();
ash::RegisterUserProfilePrefs(pref_service2->registry(), /*country=*/"",
true);
pref_service1->SetInteger(data_.pref_name, static_cast<int>(modifier_key_));
pref_service2->SetInteger(data_.pref_name, static_cast<int>(modifier_key_));
ash_test_helper()->test_session_controller_client()->SetUserPrefService(
account_id1, std::move(pref_service1));
ash_test_helper()->test_session_controller_client()->SetUserPrefService(
account_id2, std::move(pref_service2));
ResetHistogramTester();
// Sign into first account and verify the metric is emitted.
SimulateUserLogin(account_id1);
if (modifier_key_ != data_.default_modifier_key) {
histogram_tester_->ExpectUniqueSample(data_.started_metric_name,
static_cast<int>(modifier_key_), 1);
} else {
histogram_tester_->ExpectUniqueSample(data_.started_metric_name,
static_cast<int>(modifier_key_), 0);
}
// Sign into second account and verify the metric is emitted.
SimulateUserLogin(account_id2);
if (modifier_key_ != data_.default_modifier_key) {
histogram_tester_->ExpectUniqueSample(data_.started_metric_name,
static_cast<int>(modifier_key_), 2);
} else {
histogram_tester_->ExpectUniqueSample(data_.started_metric_name,
static_cast<int>(modifier_key_), 0);
}
// Sign back into the first account and verify no more metrics are emitted.
SimulateUserLogin(account_id1);
if (modifier_key_ != data_.default_modifier_key) {
histogram_tester_->ExpectUniqueSample(data_.started_metric_name,
static_cast<int>(modifier_key_), 2);
} else {
histogram_tester_->ExpectUniqueSample(data_.started_metric_name,
static_cast<int>(modifier_key_), 0);
}
}
// Contains a list of modifier remappings to apply and then the expected hash
// value after a user signs in. If `expected_value` is empty, then no metric is
// expected.
struct KeyboardModifierMetricsRecorderHashTestData {
base::flat_map<std::string, ui::mojom::ModifierKey> modifier_remappings;
std::optional<int32_t> expected_value;
};
class KeyboardModifierMetricsRecorderHashTest
: public KeyboardModifierMetricsRecorderTest,
public testing::WithParamInterface<
KeyboardModifierMetricsRecorderHashTestData> {
void SetUp() override {
KeyboardModifierMetricsRecorderTest::SetUp();
data_ = GetParam();
ResetHistogramTester();
}
protected:
KeyboardModifierMetricsRecorderHashTestData data_;
};
// Contains lists of modifier remappings to apply before signing in the user and
// then the expected computed hash once the user signs in.
INSTANTIATE_TEST_SUITE_P(
,
KeyboardModifierMetricsRecorderHashTest,
testing::ValuesIn(std::vector<KeyboardModifierMetricsRecorderHashTestData>{
// With only default remappings, no metric is expected.
{{}, std::nullopt},
// All keys remapped to `ui::mojom::ModifierKey::kMeta` should hash to
// 0.
{{{::prefs::kLanguageRemapAltKeyTo, ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapCapsLockKeyTo, ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapBackspaceKeyTo,
ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapEscapeKeyTo, ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapControlKeyTo, ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapAssistantKeyTo,
ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapExternalMetaKeyTo,
ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapExternalCommandKeyTo,
ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapSearchKeyTo, ui::mojom::ModifierKey::kMeta}},
0x0},
// All keys remapped to `ui::mojom::ModifierKey::kBackspace` should hash
// to
// 0x6db6db6.
{{{::prefs::kLanguageRemapAltKeyTo, ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapCapsLockKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapBackspaceKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapEscapeKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapControlKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapAssistantKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapExternalMetaKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapExternalCommandKeyTo,
ui::mojom::ModifierKey::kBackspace},
{::prefs::kLanguageRemapSearchKeyTo,
ui::mojom::ModifierKey::kBackspace}},
0x6db6db6},
// Random assortment of keys with a manually computed hash.
{{{::prefs::kLanguageRemapAltKeyTo, ui::mojom::ModifierKey::kControl},
{::prefs::kLanguageRemapCapsLockKeyTo, ui::mojom::ModifierKey::kAlt},
{::prefs::kLanguageRemapBackspaceKeyTo,
ui::mojom::ModifierKey::kAssistant},
{::prefs::kLanguageRemapEscapeKeyTo, ui::mojom::ModifierKey::kVoid},
{::prefs::kLanguageRemapControlKeyTo, ui::mojom::ModifierKey::kMeta},
{::prefs::kLanguageRemapAssistantKeyTo, ui::mojom::ModifierKey::kAlt},
{::prefs::kLanguageRemapExternalMetaKeyTo,
ui::mojom::ModifierKey::kControl},
{::prefs::kLanguageRemapExternalCommandKeyTo,
ui::mojom::ModifierKey::kCapsLock},
{::prefs::kLanguageRemapSearchKeyTo, ui::mojom::ModifierKey::kAlt}},
0x4452ec1},
}));
TEST_P(KeyboardModifierMetricsRecorderHashTest, HashTest) {
// Initialize two pref services with the initial value of the metric.
const AccountId account_id1 = AccountId::FromUserEmail(kUserEmail1);
const AccountId account_id2 = AccountId::FromUserEmail(kUserEmail2);
std::unique_ptr<TestingPrefServiceSimple> pref_service1 =
std::make_unique<TestingPrefServiceSimple>();
ash::RegisterUserProfilePrefs(pref_service1->registry(), /*country=*/"",
true);
std::unique_ptr<TestingPrefServiceSimple> pref_service2 =
std::make_unique<TestingPrefServiceSimple>();
ash::RegisterUserProfilePrefs(pref_service2->registry(), /*country=*/"",
true);
for (const auto& [pref, remapping] : data_.modifier_remappings) {
pref_service1->SetInteger(pref, static_cast<int>(remapping));
pref_service2->SetInteger(pref, static_cast<int>(remapping));
}
ash_test_helper()->test_session_controller_client()->SetUserPrefService(
account_id1, std::move(pref_service1));
ash_test_helper()->test_session_controller_client()->SetUserPrefService(
account_id2, std::move(pref_service2));
ResetHistogramTester();
// Sign into first account and verify the metric is emitted.
SimulateUserLogin(account_id1);
if (data_.expected_value.has_value()) {
histogram_tester_->ExpectUniqueSample(
"ChromeOS.Settings.Keyboard.Modifiers.Hash",
data_.expected_value.value(), 1);
} else {
histogram_tester_->ExpectTotalCount(
"ChromeOS.Settings.Keyboard.Modifiers.Hash", 0);
}
// Sign into second account and verify the metric is emitted.
SimulateUserLogin(account_id2);
if (data_.expected_value.has_value()) {
histogram_tester_->ExpectUniqueSample(
"ChromeOS.Settings.Keyboard.Modifiers.Hash",
data_.expected_value.value(), 2);
} else {
histogram_tester_->ExpectTotalCount(
"ChromeOS.Settings.Keyboard.Modifiers.Hash", 0);
}
ResetHistogramTester();
// Sign back into first account and verify the metric is not emitted again.
SimulateUserLogin(account_id1);
histogram_tester_->ExpectTotalCount(
"ChromeOS.Settings.Keyboard.Modifiers.Hash", 0);
}
} // namespace ash