chromium/ash/system/keyboard_brightness/keyboard_brightness_controller.cc

// Copyright 2012 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/keyboard_brightness/keyboard_brightness_controller.h"

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/login/login_screen_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/power/power_status.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "chromeos/dbus/power_manager/backlight.pb.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/session_manager_types.h"
#include "components/user_manager/known_user.h"

namespace ash {
namespace {

bool ShouldReenableKeyboardAmbientLightSensor(
    const AccountId& account_id,
    user_manager::KnownUser& known_user) {
  // Retrieve the reason.
  const int keyboard_ambient_light_sensor_disabled_reason =
      known_user
          .FindIntPath(account_id,
                       prefs::kKeyboardAmbientLightSensorDisabledReason)
          .value_or(static_cast<int>(
              power_manager::
                  AmbientLightSensorChange_Cause_USER_REQUEST_SETTINGS_APP));

  // Re-enable ambient light sensor if cause is not from settings app, or
  // restored from preference.
  switch (keyboard_ambient_light_sensor_disabled_reason) {
    case power_manager::
        AmbientLightSensorChange_Cause_USER_REQUEST_SETTINGS_APP:
    case power_manager::
        AmbientLightSensorChange_Cause_BRIGHTNESS_USER_REQUEST_SETTINGS_APP:
    case power_manager::
        AmbientLightSensorChange_Cause_RESTORED_FROM_USER_PREFERENCE:
      return false;
    default:
      return true;
  }
}

bool ShouldRestoreKeyboardAmbientLightSensor(
    const AccountId& account_id,
    user_manager::KnownUser& known_user) {
  // Retrieve the reason.
  const int keyboard_ambient_light_sensor_disabled_reason =
      known_user
          .FindIntPath(account_id, prefs::kAmbientLightSensorDisabledReason)
          .value_or(static_cast<int>(
              power_manager::
                  AmbientLightSensorChange_Cause_USER_REQUEST_SETTINGS_APP));

  // Only restore ALS when cause is from settings app.
  switch (keyboard_ambient_light_sensor_disabled_reason) {
    case power_manager::
        AmbientLightSensorChange_Cause_USER_REQUEST_SETTINGS_APP:
    case power_manager::
        AmbientLightSensorChange_Cause_BRIGHTNESS_USER_REQUEST_SETTINGS_APP:
      return true;
    default:
      return false;
  }
}

bool ShouldSaveKeyboardAmbientLightSensorForNewDevice(
    const power_manager::AmbientLightSensorChange& change) {
  return change.cause() ==
             power_manager::
                 AmbientLightSensorChange_Cause_USER_REQUEST_SETTINGS_APP ||
         change.cause() ==
             power_manager::
                 AmbientLightSensorChange_Cause_BRIGHTNESS_USER_REQUEST_SETTINGS_APP;
}

power_manager::SetAmbientLightSensorEnabledRequest_Cause
KeyboardAmbientLightSensorChangeSourceToCause(
    KeyboardAmbientLightSensorEnabledChangeSource source) {
  switch (source) {
    case KeyboardAmbientLightSensorEnabledChangeSource::kSettingsApp:
      return power_manager::
          SetAmbientLightSensorEnabledRequest_Cause_USER_REQUEST_FROM_SETTINGS_APP;
    // TODO(longbowei): Add a new cause
    // SetAmbientLightSensorEnabledRequest_Cause_SYSTEM_REENABLED to platform
    // and update.
    default:
      return power_manager::
          SetAmbientLightSensorEnabledRequest_Cause_RESTORED_FROM_USER_PREFERENCE;
  }
}

power_manager::SetBacklightBrightnessRequest_Cause
KeyboardBrightnessChangeSourceToCause(KeyboardBrightnessChangeSource source) {
  switch (source) {
    case KeyboardBrightnessChangeSource::kSettingsApp:
      return power_manager::
          SetBacklightBrightnessRequest_Cause_USER_REQUEST_FROM_SETTINGS_APP;
    case KeyboardBrightnessChangeSource::kRestoredFromUserPref:
      return power_manager::
          SetBacklightBrightnessRequest_Cause_RESTORED_FROM_USER_PREFERENCE;
    default:
      return power_manager::SetBacklightBrightnessRequest_Cause_USER_REQUEST;
  }
}

std::string GetBrightnessActionName(BrightnessAction brightness_action) {
  switch (brightness_action) {
    case BrightnessAction::kDecreaseBrightness:
      return "Decrease";
    case BrightnessAction::kIncreaseBrightness:
      return "Increase";
    case BrightnessAction::kToggleBrightness:
      return "Toggle";
    case BrightnessAction::kSetBrightness:
      return "Set";
  }
}

// Returns true if the device is currently connected to a charger.
// Note: This is the same logic that ambient_controller.cc uses.
bool IsChargerConnected() {
  DCHECK(PowerStatus::IsInitialized());
  auto* power_status = PowerStatus::Get();
  if (power_status->IsBatteryPresent()) {
    // If battery is charging, that implies sufficient power is connected. If
    // battery is not charging, return true only if an official, non-USB charger
    // is connected. This will happen if the battery is fully charged or
    // charging is delayed by Adaptive Charging.
    return power_status->IsBatteryCharging() ||
           power_status->IsMainsChargerConnected();
  }

  // Chromeboxes have no battery.
  return power_status->IsLinePowerConnected();
}

}  // namespace

KeyboardBrightnessController::KeyboardBrightnessController(
    PrefService* local_state,
    SessionControllerImpl* session_controller)
    : local_state_(local_state), session_controller_(session_controller) {
  // Add SessionController observer.
  DCHECK(session_controller_);
  session_controller_->AddObserver(this);

  // Add PowerManagerClient observer
  chromeos::PowerManagerClient* power_manager_client =
      chromeos::PowerManagerClient::Get();
  DCHECK(power_manager_client);
  power_manager_client->AddObserver(this);

  // Record whether the keyboard has a backlight for metric collection.
  power_manager_client->HasKeyboardBacklight(base::BindOnce(
      &KeyboardBrightnessController::OnReceiveHasKeyboardBacklight,
      weak_ptr_factory_.GetWeakPtr()));

  // Record whether the device has a ambient light sensor for metric collection.
  power_manager_client->HasAmbientLightSensor(base::BindOnce(
      &KeyboardBrightnessController::OnReceiveHasAmbientLightSensor,
      weak_ptr_factory_.GetWeakPtr()));

  // Add LoginScreenController observer.
  Shell::Get()->login_screen_controller()->data_dispatcher()->AddObserver(this);

  // Record a timestamp when this is constructed so last_session_change_time_ is
  // guaranteed to have a value.
  last_session_change_time_ = base::TimeTicks::Now();
}

KeyboardBrightnessController::~KeyboardBrightnessController() {
  // Remove SessionController observer.
  DCHECK(session_controller_);
  session_controller_->RemoveObserver(this);

  // Remove PowerManagerClient observer
  chromeos::PowerManagerClient::Get()->RemoveObserver(this);

  // Remove LoginScreenController observer if exists.
  LoginScreenController* login_screen_controller =
      Shell::Get()->login_screen_controller();
  LoginDataDispatcher* data_dispatcher =
      login_screen_controller ? login_screen_controller->data_dispatcher()
                              : nullptr;
  if (data_dispatcher) {
    // Remove this observer to prevent dangling pointer errors that can occur
    // in scenarios where accelerator_controller_unittest.cc reassigns Shell's
    // brightness_control_delegate_.
    data_dispatcher->RemoveObserver(this);
  }
}

// static:
void KeyboardBrightnessController::RegisterProfilePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(
      prefs::kKeyboardAmbientLightSensorLastEnabled,
      /*default_value=*/true,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
}

// SessionObserver:
void KeyboardBrightnessController::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  active_account_id_ = account_id;

  // On login, retrieve the current keyboard brightness and save it to prefs.
  HandleGetKeyboardBrightness(base::BindOnce(
      &KeyboardBrightnessController::OnReceiveKeyboardBrightnessAfterLogin,
      weak_ptr_factory_.GetWeakPtr()));
}

// SessionObserver:
void KeyboardBrightnessController::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  pref_service_ = pref_service;

  // Don't restore the ambient light sensor value if the relevant flag is
  // disabled.
  if (!features::IsKeyboardBacklightControlInSettingsEnabled()) {
    return;
  }

  // Only restore the profile-synced ambient light sensor setting if it's a
  // user's first time logging in to a new device.
  if (!session_controller_->IsUserFirstLogin()) {
    return;
  }

  // Observe the state of the synced profile pref so that the keyboard ambient
  // light sensor setting will be restored as soon as the pref finishes syncing
  // on the new device.
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  if (pref_service_) {
    pref_change_registrar_->Init(pref_service_);
    pref_change_registrar_->Add(
        prefs::kKeyboardAmbientLightSensorLastEnabled,
        base::BindRepeating(
            &KeyboardBrightnessController::
                RestoreKeyboardAmbientLightSensorSettingOnFirstLogin,
            weak_ptr_factory_.GetWeakPtr()));
  }
}

// SessionObserver:
void KeyboardBrightnessController::OnSessionStateChanged(
    session_manager::SessionState state) {
  // Whenever the SessionState changes (e.g. LOGIN_PRIMARY to ACTIVE), record
  // the timestamp.
  last_session_change_time_ = base::TimeTicks::Now();
}

// PowerManagerClient::Observer:
void KeyboardBrightnessController::KeyboardAmbientLightSensorEnabledChanged(
    const power_manager::AmbientLightSensorChange& change) {
  // In tests and during OOBE, these may not be present.
  if (!active_account_id_.has_value() || !local_state_) {
    return;
  }

  user_manager::KnownUser known_user(local_state_);

  // If the keyboard ambient light sensor was disabled, save the cause for that
  // change into a KnownUser pref. This pref can be used if we need to
  // systematically re-enable the ambient light sensor for a subset of users
  // (e.g. those who didn't manually disable the sensor from the Settings app).
  if (!change.sensor_enabled()) {
    known_user.SetPath(
        active_account_id_.value(),
        prefs::kKeyboardAmbientLightSensorDisabledReason,
        std::make_optional<base::Value>(static_cast<int>(change.cause())));
    keyboard_ambient_light_sensor_disabled_timestamp_ = base::Time::Now();
  } else {
    // If the ambient light sensor was enabled, remove the existing "disabled
    // reason" pref.
    known_user.RemovePref(active_account_id_.value(),
                          prefs::kKeyboardAmbientLightSensorDisabledReason);
  }

  // Save the current ambient light sensor enabled status into local state.
  known_user.SetPath(active_account_id_.value(),
                     prefs::kKeyboardAmbientLightSensorEnabled,
                     std::make_optional<base::Value>(change.sensor_enabled()));

  if (ShouldSaveKeyboardAmbientLightSensorForNewDevice(change)) {
    // Save a user pref new device if change is from settings app so that we can
    // restore users' ALS settings when they login to a new device.
    PrefService* primary_user_prefs =
        session_controller_->GetActivePrefService();
    if (primary_user_prefs) {
      primary_user_prefs->SetBoolean(
          prefs::kKeyboardAmbientLightSensorLastEnabled,
          change.sensor_enabled());
    }
  }
}

// PowerManagerClient::Observer:
void KeyboardBrightnessController::KeyboardBrightnessChanged(
    const power_manager::BacklightBrightnessChange& change) {
  // In tests, these may not be present.
  if (!active_account_id_.has_value() || !local_state_) {
    return;
  }

  // Save keyboard brightness change to Local State if it was caused by a user
  // request.
  if (change.cause() ==
          power_manager::BacklightBrightnessChange_Cause_USER_REQUEST ||
      change.cause() ==
          power_manager::
              BacklightBrightnessChange_Cause_USER_REQUEST_FROM_SETTINGS_APP) {
    user_manager::KnownUser known_user(local_state_);
    known_user.SetPath(active_account_id_.value(),
                       prefs::kKeyboardBrightnessPercent,
                       std::make_optional<base::Value>(change.percent()));
  }
}

// PowerManagerClient::Observer:
void KeyboardBrightnessController::SuspendImminent(
    power_manager::SuspendImminent::Reason reason) {
  if (!features::IsKeyboardBacklightControlInSettingsEnabled()) {
    return;
  }
  // In tests, these may not be present.
  if (!active_account_id_.has_value() || !local_state_) {
    return;
  }
  if (!keyboard_ambient_light_sensor_disabled_timestamp_.has_value()) {
    return;
  }

  user_manager::KnownUser known_user(local_state_);
  base::Time now = base::Time::Now();

  // Re-enable ALS if it passed local midnight.
  if (now.LocalMidnight() -
          keyboard_ambient_light_sensor_disabled_timestamp_.value()
              .LocalMidnight() >=
      base::Days(1)) {
    if (ShouldReenableKeyboardAmbientLightSensor(active_account_id_.value(),
                                                 known_user)) {
      HandleSetKeyboardAmbientLightSensorEnabled(
          true,
          KeyboardAmbientLightSensorEnabledChangeSource::kSystemReenabled);
    }
  }
}

// LoginDataDispatcher::Observer:
void KeyboardBrightnessController::OnFocusPod(const AccountId& account_id) {
  active_account_id_ = account_id;

  if (features::IsKeyboardBacklightControlInSettingsEnabled()) {
    RestoreKeyboardBrightnessSettings(account_id);
  }
}

void KeyboardBrightnessController::HandleKeyboardBrightnessDown() {
  chromeos::PowerManagerClient::Get()->DecreaseKeyboardBrightness();
  RecordHistogramForBrightnessAction(BrightnessAction::kDecreaseBrightness);
}

void KeyboardBrightnessController::HandleKeyboardBrightnessUp() {
  chromeos::PowerManagerClient::Get()->IncreaseKeyboardBrightness();
  RecordHistogramForBrightnessAction(BrightnessAction::kIncreaseBrightness);
}

void KeyboardBrightnessController::HandleToggleKeyboardBacklight() {
  chromeos::PowerManagerClient::Get()->ToggleKeyboardBacklight();
  RecordHistogramForBrightnessAction(BrightnessAction::kToggleBrightness);
}

void KeyboardBrightnessController::HandleSetKeyboardBrightness(
    double percent,
    bool gradual,
    KeyboardBrightnessChangeSource source) {
  power_manager::SetBacklightBrightnessRequest request;
  request.set_percent(percent);
  request.set_transition(
      gradual
          ? power_manager::SetBacklightBrightnessRequest_Transition_FAST
          : power_manager::SetBacklightBrightnessRequest_Transition_INSTANT);
  request.set_cause(KeyboardBrightnessChangeSourceToCause(source));
  chromeos::PowerManagerClient::Get()->SetKeyboardBrightness(request);

  // Record the brightness action only if it was not initiated by the system's
  // brightness restoration.
  if (source != KeyboardBrightnessChangeSource::kRestoredFromUserPref) {
    RecordHistogramForBrightnessAction(BrightnessAction::kSetBrightness);
  }
}

void KeyboardBrightnessController::HandleGetKeyboardAmbientLightSensorEnabled(
    base::OnceCallback<void(std::optional<bool>)> callback) {
  chromeos::PowerManagerClient::Get()->GetKeyboardAmbientLightSensorEnabled(
      std::move(callback));
}

void KeyboardBrightnessController::HandleGetKeyboardBrightness(
    base::OnceCallback<void(std::optional<double>)> callback) {
  chromeos::PowerManagerClient::Get()->GetKeyboardBrightnessPercent(
      std::move(callback));
}

void KeyboardBrightnessController::HandleSetKeyboardAmbientLightSensorEnabled(
    bool enabled,
    KeyboardAmbientLightSensorEnabledChangeSource source) {
  power_manager::SetAmbientLightSensorEnabledRequest request;
  request.set_sensor_enabled(enabled);
  request.set_cause(KeyboardAmbientLightSensorChangeSourceToCause(source));
  chromeos::PowerManagerClient::Get()->SetKeyboardAmbientLightSensorEnabled(
      request);
}

void KeyboardBrightnessController::RestoreKeyboardBrightnessSettings(
    const AccountId& account_id) {
  user_manager::KnownUser known_user(local_state_);
  bool keyboard_ambient_light_sensor_enabled_for_account = true;

  if (ShouldReenableKeyboardAmbientLightSensor(account_id, known_user)) {
    HandleSetKeyboardAmbientLightSensorEnabled(
        /*enabled=*/true,
        KeyboardAmbientLightSensorEnabledChangeSource::kSystemReenabled);
  } else {
    keyboard_ambient_light_sensor_enabled_for_account =
        known_user
            .FindBoolPath(account_id, prefs::kKeyboardAmbientLightSensorEnabled)
            .value_or(true);
    if (!keyboard_ambient_light_sensor_enabled_for_account) {
      // If the keyboard ambient light sensor is disabled, restore the user's
      // preferred keyboard brightness level.
      const std::optional<double> keyboard_brightness_for_account =
          known_user.FindPath(account_id, prefs::kKeyboardBrightnessPercent)
              ->GetIfDouble();
      if (keyboard_brightness_for_account.has_value()) {
        HandleSetKeyboardBrightness(
            keyboard_brightness_for_account.value(),
            /*gradual=*/true,
            KeyboardBrightnessChangeSource::kRestoredFromUserPref);
      }
    }
    if (ShouldRestoreKeyboardAmbientLightSensor(account_id, known_user)) {
      HandleSetKeyboardAmbientLightSensorEnabled(
          keyboard_ambient_light_sensor_enabled_for_account,
          KeyboardAmbientLightSensorEnabledChangeSource::kRestoredFromUserPref);
    }
  }

  // Record the keyboard ambient light sensor status at login.
  if (has_sensor_ && !has_keyboard_ambient_light_sensor_status_been_recorded_) {
    base::UmaHistogramBoolean(
        "ChromeOS.Keyboard.Startup.AmbientLightSensorEnabled",
        keyboard_ambient_light_sensor_enabled_for_account);
    has_keyboard_ambient_light_sensor_status_been_recorded_ = true;
  }
}

void KeyboardBrightnessController::
    RestoreKeyboardAmbientLightSensorSettingOnFirstLogin() {
  if (!features::IsKeyboardBacklightControlInSettingsEnabled() ||
      !pref_service_ ||
      has_keyboard_ambient_light_sensor_been_restored_for_new_user_) {
    return;
  }

  // Restore the keyboard ambient light sensor setting.
  const bool ambient_light_sensor_last_enabled_for_account =
      pref_service_->GetBoolean(prefs::kKeyboardAmbientLightSensorLastEnabled);
  HandleSetKeyboardAmbientLightSensorEnabled(
      ambient_light_sensor_last_enabled_for_account,
      KeyboardAmbientLightSensorEnabledChangeSource::kRestoredFromUserPref);

  has_keyboard_ambient_light_sensor_been_restored_for_new_user_ = true;
}

void KeyboardBrightnessController::OnReceiveHasKeyboardBacklight(
    std::optional<bool> has_keyboard_backlight) {
  if (has_keyboard_backlight.has_value()) {
    base::UmaHistogramBoolean("ChromeOS.Keyboard.HasBacklight",
                              has_keyboard_backlight.value());
    return;
  }
  LOG(ERROR) << "KeyboardBrightnessController: Failed to get the keyboard "
                "backlight status";
}

void KeyboardBrightnessController::OnReceiveHasAmbientLightSensor(
    std::optional<bool> has_sensor) {
  if (!has_sensor.has_value()) {
    LOG(ERROR)
        << "KeyboardBrightnessController: Failed to get the ambient light "
           "sensor status";
    return;
  }
  has_sensor_ = has_sensor.value();
  base::UmaHistogramBoolean("ChromeOS.Keyboard.HasAmbientLightSensor",
                            has_sensor.value());
}

void KeyboardBrightnessController::OnReceiveKeyboardBrightnessAfterLogin(
    std::optional<double> keyboard_brightness) {
  // In tests, these may not be present.
  if (!active_account_id_.has_value() || !local_state_) {
    return;
  }

  if (!keyboard_brightness.has_value()) {
    LOG(ERROR) << "KeyboardBrightnessController: keyboard_brightness has no "
                  "value, so cannot set prefs.";
    return;
  }

  // Save keyboard brightness to local state after login.
  user_manager::KnownUser known_user(local_state_);
  known_user.SetPath(
      active_account_id_.value(), prefs::kKeyboardBrightnessPercent,
      std::make_optional<base::Value>(keyboard_brightness.value()));
}

void KeyboardBrightnessController::RecordHistogramForBrightnessAction(
    BrightnessAction brightness_action) {
  // Only record the first brightness adjustment (resets on reboot).
  if (has_brightness_been_adjusted_) {
    return;
  }
  has_brightness_been_adjusted_ = true;

  CHECK(!last_session_change_time_.is_null());

  const base::TimeDelta time_since_last_session_change =
      base::TimeTicks::Now() - last_session_change_time_;

  // Don't record a metric if the first brightness adjustment occurred >1 hour
  // after the last session change.
  if (time_since_last_session_change >= base::Hours(1)) {
    return;
  }

  const session_manager::SessionState session_state =
      session_controller_->GetSessionState();
  const bool is_on_login_screen =
      session_state == session_manager::SessionState::LOGIN_PRIMARY ||
      session_state == session_manager::SessionState::LOGIN_SECONDARY;
  const bool is_active_session =
      session_state == session_manager::SessionState::ACTIVE;

  // Disregard brightness events that don't occur on the login screen or in an
  // active user session.
  if (!(is_on_login_screen || is_active_session)) {
    return;
  }

  base::UmaHistogramLongTimes100(
      base::StrCat({"ChromeOS.Keyboard.TimeUntilFirstBrightnessChange.",
                    is_on_login_screen ? "OnLoginScreen" : "AfterLogin", ".",
                    GetBrightnessActionName(brightness_action), "Brightness.",
                    IsChargerConnected() ? "Charger" : "Battery", "Power"}),
      time_since_last_session_change);
}

}  // namespace ash