chromium/ash/system/input_device_settings/pref_handlers/touchpad_pref_handler_impl.cc

// 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/pref_handlers/touchpad_pref_handler_impl.h"

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/mojom/input_device_settings.mojom-forward.h"
#include "ash/public/mojom/input_device_settings.mojom.h"
#include "ash/shell.h"
#include "ash/system/input_device_settings/input_device_settings_defaults.h"
#include "ash/system/input_device_settings/input_device_settings_pref_names.h"
#include "ash/system/input_device_settings/input_device_settings_utils.h"
#include "ash/system/input_device_settings/input_device_tracker.h"
#include "ash/system/input_device_settings/settings_updated_metrics_info.h"
#include "base/check.h"
#include "base/json/values_util.h"
#include "base/values.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/known_user.h"
#include "ui/events/ash/mojom/simulate_right_click_modifier.mojom-shared.h"

namespace ash {
namespace {

// Whether or not settings taken during the transition period should be
// persisted to the prefs. Values should only ever be true if the original
// setting was a user-configured value.
struct ForceTouchpadSettingPersistence {
  bool sensitivity = false;
  bool reverse_scrolling = false;
  bool acceleration_enabled = false;
  bool tap_to_click_enabled = false;
  bool tap_dragging_enabled = false;
  bool scroll_sensitivity = false;
  bool scroll_acceleration = false;
  bool haptic_sensitivity = false;
  bool haptic_enabled = false;
  bool three_finger_click_enabled = false;
};

ui::mojom::SimulateRightClickModifier GetSimulateRightClickModifierFromPrefs(
    PrefService* prefs) {
  if (!prefs) {
    return kDefaultSimulateRightClick;
  }

  const auto* alt_right_click_preference =
      prefs->GetUserPrefValue(prefs::kAltEventRemappedToRightClick);
  const auto* search_right_click_preference =
      prefs->GetUserPrefValue(prefs::kSearchEventRemappedToRightClick);
  const auto alt_count =
      alt_right_click_preference ? alt_right_click_preference->GetInt() : 0;
  const auto search_count = search_right_click_preference
                                ? search_right_click_preference->GetInt()
                                : 0;
  // Disable (Alt/Search+Click) remapping if the user never performs this
  // action.
  if (alt_count == 0 && search_count == 0) {
    return ui::mojom::SimulateRightClickModifier::kNone;
  }

  // Return the modifier used more frequently, in case of a tie, Search will
  // be preferred to avoid Alt-based issues.
  return search_count >= alt_count
             ? ui::mojom::SimulateRightClickModifier::kSearch
             : ui::mojom::SimulateRightClickModifier::kAlt;
}

// GetTouchpadSettingsFromPrefs returns a touchpad settings based on user prefs
// to be used as settings for new touchpads.
mojom::TouchpadSettingsPtr GetTouchpadSettingsFromPrefs(
    PrefService* prefs,
    ForceTouchpadSettingPersistence& force_persistence) {
  mojom::TouchpadSettingsPtr settings = mojom::TouchpadSettings::New();

  const auto* sensitivity_preference =
      prefs->GetUserPrefValue(prefs::kTouchpadSensitivity);
  settings->sensitivity = sensitivity_preference
                              ? sensitivity_preference->GetInt()
                              : kDefaultSensitivity;
  force_persistence.sensitivity = sensitivity_preference != nullptr;

  const auto* reverse_scrolling_preference =
      prefs->GetUserPrefValue(prefs::kNaturalScroll);
  settings->reverse_scrolling = reverse_scrolling_preference
                                    ? reverse_scrolling_preference->GetBool()
                                    : kDefaultReverseScrolling;
  force_persistence.reverse_scrolling = reverse_scrolling_preference != nullptr;

  const auto* acceleration_enabled_preference =
      prefs->GetUserPrefValue(prefs::kTouchpadAcceleration);
  settings->acceleration_enabled =
      acceleration_enabled_preference
          ? acceleration_enabled_preference->GetBool()
          : kDefaultAccelerationEnabled;
  force_persistence.acceleration_enabled =
      acceleration_enabled_preference != nullptr;

  const auto* tap_to_click_enabled_preference =
      prefs->GetUserPrefValue(prefs::kTapToClickEnabled);
  settings->tap_to_click_enabled =
      tap_to_click_enabled_preference
          ? tap_to_click_enabled_preference->GetBool()
          : kDefaultTapToClickEnabled;
  force_persistence.tap_to_click_enabled =
      tap_to_click_enabled_preference != nullptr;

  // Three finger click does not update `force_persistence` as it will soon be
  // removed.
  const auto* three_finger_click_enabled_preference =
      prefs->GetUserPrefValue(prefs::kEnableTouchpadThreeFingerClick);
  settings->three_finger_click_enabled =
      three_finger_click_enabled_preference
          ? three_finger_click_enabled_preference->GetBool()
          : kDefaultThreeFingerClickEnabled;

  const auto* tap_dragging_enabled_preference =
      prefs->GetUserPrefValue(prefs::kTapDraggingEnabled);
  settings->tap_dragging_enabled =
      tap_dragging_enabled_preference
          ? tap_dragging_enabled_preference->GetBool()
          : kDefaultTapDraggingEnabled;
  force_persistence.tap_dragging_enabled =
      tap_dragging_enabled_preference != nullptr;

  const auto* scroll_sensitivity_preference =
      prefs->GetUserPrefValue(prefs::kTouchpadScrollSensitivity);
  settings->scroll_sensitivity = scroll_sensitivity_preference
                                     ? scroll_sensitivity_preference->GetInt()
                                     : kDefaultScrollSensitivity;
  force_persistence.scroll_sensitivity =
      scroll_sensitivity_preference != nullptr;

  const auto* scroll_acceleration_preference =
      prefs->GetUserPrefValue(prefs::kTouchpadScrollAcceleration);
  settings->scroll_acceleration =
      scroll_acceleration_preference ? scroll_acceleration_preference->GetBool()
                                     : kDefaultScrollAccelerationEnabled;
  force_persistence.scroll_acceleration =
      scroll_acceleration_preference != nullptr;

  const auto* haptic_sensitivity_preference =
      prefs->GetUserPrefValue(prefs::kTouchpadHapticClickSensitivity);
  settings->haptic_sensitivity = haptic_sensitivity_preference
                                     ? haptic_sensitivity_preference->GetInt()
                                     : kDefaultHapticSensitivity;
  force_persistence.haptic_sensitivity =
      haptic_sensitivity_preference != nullptr;

  const auto* haptic_enabled_preference =
      prefs->GetUserPrefValue(prefs::kTouchpadHapticFeedback);
  settings->haptic_enabled = haptic_enabled_preference
                                 ? haptic_enabled_preference->GetBool()
                                 : kDefaultHapticFeedbackEnabled;
  force_persistence.haptic_enabled = haptic_enabled_preference != nullptr;

  if (features::IsAltClickAndSixPackCustomizationEnabled()) {
    settings->simulate_right_click =
        GetSimulateRightClickModifierFromPrefs(prefs);
  }
  return settings;
}

mojom::TouchpadSettingsPtr RetrieveTouchpadSettings(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad,
    const base::Value::Dict& settings_dict) {
  mojom::TouchpadSettingsPtr settings = mojom::TouchpadSettings::New();
  settings->sensitivity =
      settings_dict.FindInt(prefs::kTouchpadSettingSensitivity)
          .value_or(kDefaultSensitivity);
  settings->reverse_scrolling =
      settings_dict.FindBool(prefs::kTouchpadSettingReverseScrolling)
          .value_or(kDefaultReverseScrolling);
  settings->acceleration_enabled =
      settings_dict.FindBool(prefs::kTouchpadSettingAccelerationEnabled)
          .value_or(kDefaultAccelerationEnabled);
  settings->tap_to_click_enabled =
      settings_dict.FindBool(prefs::kTouchpadSettingTapToClickEnabled)
          .value_or(kDefaultTapToClickEnabled);
  settings->three_finger_click_enabled =
      settings_dict.FindBool(prefs::kTouchpadSettingThreeFingerClickEnabled)
          .value_or(kDefaultThreeFingerClickEnabled);
  settings->tap_dragging_enabled =
      settings_dict.FindBool(prefs::kTouchpadSettingTapDraggingEnabled)
          .value_or(kDefaultTapDraggingEnabled);
  settings->scroll_sensitivity =
      settings_dict.FindInt(prefs::kTouchpadSettingScrollSensitivity)
          .value_or(kDefaultScrollSensitivity);
  settings->scroll_acceleration =
      settings_dict.FindBool(prefs::kTouchpadSettingScrollAcceleration)
          .value_or(kDefaultScrollAccelerationEnabled);

  if (touchpad.is_haptic) {
    settings->haptic_sensitivity =
        settings_dict.FindInt(prefs::kTouchpadSettingHapticSensitivity)
            .value_or(kDefaultSensitivity);
    settings->haptic_enabled =
        settings_dict.FindBool(prefs::kTouchpadSettingHapticEnabled)
            .value_or(kDefaultHapticFeedbackEnabled);
  } else {
    settings->haptic_sensitivity = kDefaultSensitivity;
    settings->haptic_enabled = kDefaultHapticFeedbackEnabled;
  }

  if (features::IsAltClickAndSixPackCustomizationEnabled()) {
    settings->simulate_right_click =
        static_cast<ui::mojom::SimulateRightClickModifier>(
            settings_dict.FindInt(prefs::kTouchpadSettingSimulateRightClick)
                .value_or(static_cast<int>(
                    GetSimulateRightClickModifierFromPrefs(pref_service))));
  }

  return settings;
}

mojom::TouchpadSettingsPtr GetDefaultTouchpadSettings(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad) {
  if (pref_service) {
    return RetrieveTouchpadSettings(
        pref_service, touchpad,
        pref_service->GetDict(prefs::kTouchpadDefaultSettings));
  }

  return RetrieveTouchpadSettings(pref_service, touchpad, /*settings_dict=*/{});
}

base::Value::Dict ConvertSettingsToDict(
    const mojom::Touchpad& touchpad,
    const ForceTouchpadSettingPersistence& force_persistence,
    const base::Value::Dict* existing_settings_dict) {
  // Populate `settings_dict` with all settings in `settings`.
  base::Value::Dict settings_dict;

  if (ShouldPersistSetting(prefs::kTouchpadSettingSensitivity,
                           static_cast<int>(touchpad.settings->sensitivity),
                           kDefaultSensitivity, force_persistence.sensitivity,
                           existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingSensitivity,
                      touchpad.settings->sensitivity);
  }

  if (ShouldPersistSetting(
          prefs::kTouchpadSettingReverseScrolling,
          touchpad.settings->reverse_scrolling, kDefaultReverseScrolling,
          force_persistence.reverse_scrolling, existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingReverseScrolling,
                      touchpad.settings->reverse_scrolling);
  }

  if (ShouldPersistSetting(
          prefs::kTouchpadSettingAccelerationEnabled,
          touchpad.settings->acceleration_enabled, kDefaultAccelerationEnabled,
          force_persistence.acceleration_enabled, existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingAccelerationEnabled,
                      touchpad.settings->acceleration_enabled);
  }

  if (ShouldPersistSetting(
          prefs::kTouchpadSettingScrollSensitivity,
          static_cast<int>(touchpad.settings->scroll_sensitivity),
          kDefaultScrollSensitivity, force_persistence.scroll_sensitivity,
          existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingScrollSensitivity,
                      touchpad.settings->scroll_sensitivity);
  }

  if (ShouldPersistSetting(prefs::kTouchpadSettingScrollAcceleration,
                           touchpad.settings->scroll_acceleration,
                           kDefaultScrollAccelerationEnabled,
                           force_persistence.scroll_acceleration,
                           existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingScrollAcceleration,
                      touchpad.settings->scroll_acceleration);
  }

  if (ShouldPersistSetting(
          prefs::kTouchpadSettingTapToClickEnabled,
          touchpad.settings->tap_to_click_enabled, kDefaultTapToClickEnabled,
          force_persistence.tap_to_click_enabled, existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingTapToClickEnabled,
                      touchpad.settings->tap_to_click_enabled);
  }

  if (ShouldPersistSetting(prefs::kTouchpadSettingThreeFingerClickEnabled,
                           touchpad.settings->three_finger_click_enabled,
                           kDefaultThreeFingerClickEnabled,
                           force_persistence.three_finger_click_enabled,
                           existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingThreeFingerClickEnabled,
                      touchpad.settings->three_finger_click_enabled);
  }

  if (ShouldPersistSetting(
          prefs::kTouchpadSettingTapDraggingEnabled,
          touchpad.settings->tap_dragging_enabled, kDefaultTapDraggingEnabled,
          force_persistence.tap_dragging_enabled, existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingTapDraggingEnabled,
                      touchpad.settings->tap_dragging_enabled);
  }

  if (touchpad.is_haptic &&
      ShouldPersistSetting(
          prefs::kTouchpadSettingHapticSensitivity,
          static_cast<int>(touchpad.settings->haptic_sensitivity),
          kDefaultSensitivity, force_persistence.haptic_sensitivity,
          existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingHapticSensitivity,
                      touchpad.settings->haptic_sensitivity);
  }

  if (touchpad.is_haptic &&
      ShouldPersistSetting(
          prefs::kTouchpadSettingHapticEnabled,
          touchpad.settings->haptic_enabled, kDefaultHapticFeedbackEnabled,
          force_persistence.haptic_enabled, existing_settings_dict)) {
    settings_dict.Set(prefs::kTouchpadSettingHapticEnabled,
                      touchpad.settings->haptic_enabled);
  }

  if (features::IsAltClickAndSixPackCustomizationEnabled()) {
    settings_dict.Set(
        prefs::kTouchpadSettingSimulateRightClick,
        static_cast<int>(touchpad.settings->simulate_right_click));
  }
  return settings_dict;
}

void UpdateInternalTouchpadSettingsImpl(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad,
    const ForceTouchpadSettingPersistence& force_persistence) {
  CHECK(touchpad.settings);
  CHECK(!touchpad.is_external);

  base::Value::Dict existing_settings_dict =
      pref_service->GetDict(prefs::kTouchpadInternalSettings).Clone();
  base::Value::Dict settings_dict = ConvertSettingsToDict(
      touchpad, force_persistence, &existing_settings_dict);

  // Merge the new settings into the old settings so that all settings are
  // transferred over (including ones that might not work on the current
  // touchpad such as haptic settings)
  existing_settings_dict.Merge(std::move(settings_dict));
  pref_service->SetDict(prefs::kTouchpadInternalSettings,
                        std::move(existing_settings_dict));
}

void UpdateTouchpadSettingsImpl(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad,
    const ForceTouchpadSettingPersistence& force_persistence) {
  CHECK(touchpad.settings);

  if (!touchpad.is_external) {
    UpdateInternalTouchpadSettingsImpl(pref_service, touchpad,
                                       force_persistence);
    return;
  }

  base::Value::Dict devices_dict =
      pref_service->GetDict(prefs::kTouchpadDeviceSettingsDictPref).Clone();
  base::Value::Dict* existing_settings_dict =
      devices_dict.FindDict(touchpad.device_key);

  base::Value::Dict settings_dict = ConvertSettingsToDict(
      touchpad, force_persistence, existing_settings_dict);
  const base::Time time_stamp = base::Time::Now();
  settings_dict.Set(prefs::kLastUpdatedKey, base::TimeToValue(time_stamp));

  // If an old settings dict already exists for the device, merge the updated
  // settings into the old settings. Otherwise, insert the dict at
  // `touchpad.device_key`.
  if (existing_settings_dict) {
    existing_settings_dict->Merge(std::move(settings_dict));
  } else {
    devices_dict.Set(touchpad.device_key, std::move(settings_dict));
  }

  pref_service->SetDict(std::string(prefs::kTouchpadDeviceSettingsDictPref),
                        std::move(devices_dict));
}

mojom::TouchpadSettingsPtr GetTouchpadSettingsFromOldLocalStatePrefs(
    PrefService* local_state,
    const AccountId& account_id,
    const mojom::Touchpad& touchpad) {
  mojom::TouchpadSettingsPtr settings =
      GetDefaultTouchpadSettings(/*pref_service=*/nullptr, touchpad);
  settings->tap_to_click_enabled =
      user_manager::KnownUser(local_state)
          .FindBoolPath(account_id, prefs::kOwnerTapToClickEnabled)
          .value_or(kDefaultTapToClickEnabled);

  return settings;
}

bool HasDefaultSettings(PrefService* pref_service) {
  const auto* pref =
      pref_service->FindPreference(prefs::kTouchpadDefaultSettings);
  return pref && pref->HasUserSetting();
}

void InitializeSettingsUpdateMetricInfo(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad,
    SettingsUpdatedMetricsInfo::Category category) {
  CHECK(pref_service);

  const auto& settings_metric_info =
      pref_service->GetDict(prefs::kTouchpadUpdateSettingsMetricInfo);
  const auto* device_metric_info =
      settings_metric_info.Find(touchpad.device_key);
  if (device_metric_info) {
    return;
  }

  auto updated_metric_info = settings_metric_info.Clone();

  const SettingsUpdatedMetricsInfo metrics_info(category, base::Time::Now());
  updated_metric_info.Set(touchpad.device_key, metrics_info.ToDict());

  pref_service->SetDict(prefs::kTouchpadUpdateSettingsMetricInfo,
                        std::move(updated_metric_info));
}

void InitializeTouchpadSettingsImpl(PrefService* pref_service,
                                    mojom::Touchpad* touchpad,
                                    bool force_initialize_to_default_settings) {
  if (!pref_service) {
    touchpad->settings = GetDefaultTouchpadSettings(pref_service, *touchpad);
    return;
  }

  const base::Value::Dict* settings_dict = nullptr;
  if (!touchpad->is_external) {
    settings_dict = &pref_service->GetDict(prefs::kTouchpadInternalSettings);
    if (settings_dict->empty()) {
      settings_dict = nullptr;
    }
  } else {
    const auto& devices_dict =
        pref_service->GetDict(prefs::kTouchpadDeviceSettingsDictPref);
    settings_dict = devices_dict.FindDict(touchpad->device_key);
  }

  // Do not lookup settings dict if we are force refreshing back to default
  // settings.
  if (force_initialize_to_default_settings) {
    settings_dict = nullptr;
  }

  ForceTouchpadSettingPersistence force_persistence;
  SettingsUpdatedMetricsInfo::Category category;
  if (settings_dict) {
    category = SettingsUpdatedMetricsInfo::Category::kSynced;
    touchpad->settings =
        RetrieveTouchpadSettings(pref_service, *touchpad, *settings_dict);
  } else if (Shell::Get()->input_device_tracker()->WasDevicePreviouslyConnected(
                 InputDeviceTracker::InputDeviceCategory::kTouchpad,
                 touchpad->device_key)) {
    category = SettingsUpdatedMetricsInfo::Category::kDefault;
    touchpad->settings =
        GetTouchpadSettingsFromPrefs(pref_service, force_persistence);
  } else {
    category = HasDefaultSettings(pref_service)
                   ? SettingsUpdatedMetricsInfo::Category::kDefault
                   : SettingsUpdatedMetricsInfo::Category::kFirstEver;
    touchpad->settings = GetDefaultTouchpadSettings(pref_service, *touchpad);
  }
  DCHECK(touchpad->settings);
  InitializeSettingsUpdateMetricInfo(pref_service, *touchpad, category);

  UpdateTouchpadSettingsImpl(pref_service, *touchpad, force_persistence);
}

}  // namespace

TouchpadPrefHandlerImpl::TouchpadPrefHandlerImpl() = default;
TouchpadPrefHandlerImpl::~TouchpadPrefHandlerImpl() = default;

void TouchpadPrefHandlerImpl::InitializeTouchpadSettings(
    PrefService* pref_service,
    mojom::Touchpad* touchpad) {
  InitializeTouchpadSettingsImpl(
      pref_service, touchpad,
      /*force_initialize_to_default_settings=*/false);
}

void TouchpadPrefHandlerImpl::UpdateTouchpadSettings(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad) {
  UpdateTouchpadSettingsImpl(pref_service, touchpad,
                             /*force_persistence=*/{});
}

void TouchpadPrefHandlerImpl::InitializeLoginScreenTouchpadSettings(
    PrefService* local_state,
    const AccountId& account_id,
    mojom::Touchpad* touchpad) {
  // Verify if the flag is enabled.
  if (!features::IsInputDeviceSettingsSplitEnabled()) {
    return;
  }
  CHECK(local_state);

  const auto* settings_dict = GetLoginScreenSettingsDict(
      local_state, account_id,
      touchpad->is_external ? prefs::kTouchpadLoginScreenExternalSettingsPref
                            : prefs::kTouchpadLoginScreenInternalSettingsPref);
  if (settings_dict) {
    touchpad->settings = RetrieveTouchpadSettings(/*pref_service=*/nullptr,
                                                  *touchpad, *settings_dict);
  } else {
    touchpad->settings = GetTouchpadSettingsFromOldLocalStatePrefs(
        local_state, account_id, *touchpad);
  }

  if (features::IsAltClickAndSixPackCustomizationEnabled()) {
    touchpad->settings->simulate_right_click = kDefaultSimulateRightClick;
  }
}

void TouchpadPrefHandlerImpl::UpdateLoginScreenTouchpadSettings(
    PrefService* local_state,
    const AccountId& account_id,
    const mojom::Touchpad& touchpad) {
  CHECK(local_state);
  const auto* pref_name = touchpad.is_external
                              ? prefs::kTouchpadLoginScreenExternalSettingsPref
                              : prefs::kTouchpadLoginScreenInternalSettingsPref;
  auto* settings_dict =
      GetLoginScreenSettingsDict(local_state, account_id, pref_name);

  user_manager::KnownUser(local_state)
      .SetPath(account_id, pref_name,
               std::make_optional<base::Value>(ConvertSettingsToDict(
                   touchpad, /*force_persistence=*/{}, settings_dict)));
}

void TouchpadPrefHandlerImpl::InitializeWithDefaultTouchpadSettings(
    mojom::Touchpad* touchpad) {
  touchpad->settings =
      GetDefaultTouchpadSettings(/*pref_service=*/nullptr, *touchpad);
}

void TouchpadPrefHandlerImpl::UpdateDefaultTouchpadSettings(
    PrefService* pref_service,
    const mojom::Touchpad& touchpad) {
  // All settings should be persisted fully when storing defaults.
  auto settings_dict =
      ConvertSettingsToDict(touchpad, /*force_persistence=*/{true},
                            /*existing_settings_dict=*/nullptr);
  pref_service->SetDict(prefs::kTouchpadDefaultSettings,
                        std::move(settings_dict));
}

void TouchpadPrefHandlerImpl::ForceInitializeWithDefaultSettings(
    PrefService* pref_service,
    mojom::Touchpad* touchpad) {
  InitializeTouchpadSettingsImpl(pref_service, touchpad,
                                 /*force_initialize_to_default_settings=*/true);
}

}  // namespace ash