chromium/ash/system/brightness/brightness_controller_chromeos.cc

// Copyright 2013 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/brightness/brightness_controller_chromeos.h"

#include <optional>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/login/login_screen_controller.h"
#include "ash/login/ui/login_data_dispatcher.h"
#include "ash/login_status.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/brightness_control_delegate.h"
#include "ash/system/power/power_status.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/time/time.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_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/session_manager_types.h"
#include "components/user_manager/known_user.h"

namespace ash::system {

namespace {

std::string GetBrightnessActionName(BrightnessAction brightness_action) {
  switch (brightness_action) {
    case BrightnessAction::kDecreaseBrightness:
      return "Decrease";
    case BrightnessAction::kIncreaseBrightness:
      return "Increase";
    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();
}

void SaveBrightnessPercentToLocalState(PrefService* local_state,
                                       const AccountId& account_id,
                                       double percent) {
  user_manager::KnownUser known_user(local_state);
  known_user.SetPath(account_id, prefs::kInternalDisplayScreenBrightnessPercent,
                     std::make_optional<base::Value>(percent));
}

bool ShouldReenableAmbientLightSensor(const AccountId& account_id,
                                      user_manager::KnownUser& known_user) {
  // Retrieve the reason.
  const int 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));

  // Re-enable ambient light sensor if cause is not from settings app or
  // restored from preference.
  switch (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 ShouldRestoreAmbientLightSensor(const AccountId& account_id,
                                     user_manager::KnownUser& known_user) {
  // Retrieve the reason.
  const int 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 (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 ShouldSaveAmbientLightSensorForNewDevice(
    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
AmbientLightSensorChangeSourceToCause(
    BrightnessControlDelegate::AmbientLightSensorEnabledChangeSource source) {
  switch (source) {
    case BrightnessControlDelegate::AmbientLightSensorEnabledChangeSource::
        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 function.
    default:
      return power_manager::
          SetAmbientLightSensorEnabledRequest_Cause_RESTORED_FROM_USER_PREFERENCE;
  }
}

power_manager::SetBacklightBrightnessRequest_Cause
BrightnessChangeSourceToCause(
    BrightnessControlDelegate::BrightnessChangeSource source) {
  switch (source) {
    case BrightnessControlDelegate::BrightnessChangeSource::kSettingsApp:
      return power_manager::
          SetBacklightBrightnessRequest_Cause_USER_REQUEST_FROM_SETTINGS_APP;
    case BrightnessControlDelegate::BrightnessChangeSource::
        kRestoredFromUserPref:
      return power_manager::
          SetBacklightBrightnessRequest_Cause_RESTORED_FROM_USER_PREFERENCE;
    default:
      return power_manager::SetBacklightBrightnessRequest_Cause_USER_REQUEST;
  }
}

PrefService* GetActivePrefService() {
  if (!ash::Shell::HasInstance()) {
    return nullptr;
  }
  DCHECK(ash::Shell::Get()->session_controller());
  return ash::Shell::Get()->session_controller()->GetActivePrefService();
}

}  // namespace

BrightnessControllerChromeos::BrightnessControllerChromeos(
    PrefService* local_state,
    SessionControllerImpl* session_controller)
    : local_state_(local_state), session_controller_(session_controller) {
  chromeos::PowerManagerClient* power_manager_client =
      chromeos::PowerManagerClient::Get();
  DCHECK(power_manager_client);
  power_manager_client->AddObserver(this);
  power_manager_client->HasAmbientLightSensor(
      base::BindOnce(&BrightnessControllerChromeos::OnGetHasAmbientLightSensor,
                     weak_ptr_factory_.GetWeakPtr()));

  DCHECK(session_controller_);
  session_controller_->AddObserver(this);

  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();
}

BrightnessControllerChromeos::~BrightnessControllerChromeos() {
  chromeos::PowerManagerClient::Get()->RemoveObserver(this);

  DCHECK(session_controller_);
  session_controller_->RemoveObserver(this);

  LoginScreenController* login_screen_controller =
      Shell::Get()->login_screen_controller();
  LoginDataDispatcher* data_dispatcher =
      login_screen_controller ? login_screen_controller->data_dispatcher()
                              : nullptr;
  // Typically, brightness_control_delegate is destroyed after
  // login_screen_controller in shell.cc (which is why we check to see if
  // login_screen_controller still exists), so it's not necessary to remove the
  // observer. However, accelerator_controller_unittest.cc reassigns Shell's
  // brightness_control_delegate_, which causes a dangling raw_ptr error unless
  // the observer is removed here.
  if (data_dispatcher) {
    data_dispatcher->RemoveObserver(this);
  }
}

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

void BrightnessControllerChromeos::HandleBrightnessDown() {
  chromeos::PowerManagerClient::Get()->DecreaseScreenBrightness(true);
  RecordHistogramForBrightnessAction(BrightnessAction::kDecreaseBrightness);
}

void BrightnessControllerChromeos::HandleBrightnessUp() {
  chromeos::PowerManagerClient::Get()->IncreaseScreenBrightness();
  RecordHistogramForBrightnessAction(BrightnessAction::kIncreaseBrightness);
}

void BrightnessControllerChromeos::SetBrightnessPercent(
    double percent,
    bool gradual,
    BrightnessChangeSource 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(BrightnessChangeSourceToCause(source));
  chromeos::PowerManagerClient::Get()->SetScreenBrightness(request);

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

void BrightnessControllerChromeos::GetBrightnessPercent(
    base::OnceCallback<void(std::optional<double>)> callback) {
  chromeos::PowerManagerClient::Get()->GetScreenBrightnessPercent(
      std::move(callback));
}

void BrightnessControllerChromeos::SetAmbientLightSensorEnabled(
    bool enabled,
    AmbientLightSensorEnabledChangeSource source) {
  power_manager::SetAmbientLightSensorEnabledRequest request;
  request.set_sensor_enabled(enabled);
  request.set_cause(AmbientLightSensorChangeSourceToCause(source));
  chromeos::PowerManagerClient::Get()->SetAmbientLightSensorEnabled(request);
}

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

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

void BrightnessControllerChromeos::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 BrightnessControllerChromeos::SuspendImminent(
    power_manager::SuspendImminent::Reason reason) {
  if (!features::IsBrightnessControlInSettingsEnabled()) {
    return;
  }
  // In tests, these may not be present.
  if (!active_account_id_.has_value() || !local_state_) {
    return;
  }
  if (!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() -
          ambient_light_sensor_disabled_timestamp_.value().LocalMidnight() >=
      base::Days(1)) {
    if (ShouldReenableAmbientLightSensor(active_account_id_.value(),
                                         known_user)) {
      SetAmbientLightSensorEnabled(
          true, AmbientLightSensorEnabledChangeSource::kSystemReenabled);
    }
  }
}

void BrightnessControllerChromeos::OnFocusPod(const AccountId& account_id) {
  active_account_id_ = account_id;
  if (!features::IsBrightnessControlInSettingsEnabled()) {
    return;
  }
  if (IsInitialBrightnessSetByPolicy()) {
    return;
  }
  RestoreBrightnessSettings(account_id);
}

void BrightnessControllerChromeos::RestoreBrightnessSettings(
    const AccountId& account_id) {
  user_manager::KnownUser known_user(local_state_);
  bool ambient_light_sensor_enabled_for_account = true;

  if (ShouldReenableAmbientLightSensor(account_id, known_user)) {
    SetAmbientLightSensorEnabled(
        /*enabled=*/true,
        AmbientLightSensorEnabledChangeSource::kSystemReenabled);
  } else {
    // Get the user's stored preference for whether the ambient light sensor
    // should be enabled. If there is no saved preference for the ambient light
    // sensor value, set the ambient light sensor to be enabled to match the
    // default behavior.
    ambient_light_sensor_enabled_for_account =
        known_user
            .FindBoolPath(account_id, prefs::kDisplayAmbientLightSensorEnabled)
            .value_or(true);

    if (!ambient_light_sensor_enabled_for_account) {
      // If the ambient light sensor is disabled, restore the user's preferred
      // brightness level.
      const std::optional<double> brightness_for_account =
          known_user
              .FindPath(account_id,
                        prefs::kInternalDisplayScreenBrightnessPercent)
              ->GetIfDouble();
      if (brightness_for_account.has_value()) {
        SetBrightnessPercent(brightness_for_account.value(), /*gradual=*/true,
                             BrightnessControlDelegate::BrightnessChangeSource::
                                 kRestoredFromUserPref);
      }
    }

    if (ShouldRestoreAmbientLightSensor(account_id, known_user)) {
      SetAmbientLightSensorEnabled(
          ambient_light_sensor_enabled_for_account,
          BrightnessControlDelegate::AmbientLightSensorEnabledChangeSource::
              kRestoredFromUserPref);
    }
  }

  // Record the display ambient light sensor status at login.
  if (has_sensor_ && !has_ambient_light_sensor_status_been_recorded_) {
    base::UmaHistogramBoolean(
        "ChromeOS.Display.Startup.AmbientLightSensorEnabled",
        ambient_light_sensor_enabled_for_account);
    has_ambient_light_sensor_status_been_recorded_ = true;
  }
}

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

  if (!active_pref_service_) {
    return;
  }

  if (IsInitialBrightnessSetByPolicy()) {
    return;
  }

  // If the ambient light sensor status has already been restored, don't restore
  // it again for this device.
  if (has_ambient_light_sensor_been_restored_for_new_user_) {
    return;
  }

  // This pref has a value of true by default.
  const bool ambient_light_sensor_previously_enabled_for_account =
      active_pref_service_->GetBoolean(
          prefs::kDisplayAmbientLightSensorLastEnabled);

  SetAmbientLightSensorEnabled(
      ambient_light_sensor_previously_enabled_for_account,
      BrightnessControlDelegate::AmbientLightSensorEnabledChangeSource::
          kRestoredFromUserPref);

  has_ambient_light_sensor_been_restored_for_new_user_ = true;
}

void BrightnessControllerChromeos::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  active_pref_service_ = pref_service;

  // Don't restore the ambient light sensor value if the relevant flag is
  // disabled.
  if (!features::IsBrightnessControlInSettingsEnabled()) {
    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 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 (active_pref_service_) {
    pref_change_registrar_->Init(active_pref_service_);
    pref_change_registrar_->Add(
        prefs::kDisplayAmbientLightSensorLastEnabled,
        base::BindRepeating(&BrightnessControllerChromeos::
                                RestoreBrightnessSettingsOnFirstLogin,
                            weak_ptr_factory_.GetWeakPtr()));
  }
}

void BrightnessControllerChromeos::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  active_account_id_ = account_id;

  // On login, retrieve the current brightness and save it to prefs.
  GetBrightnessPercent(
      base::BindOnce(&BrightnessControllerChromeos::OnGetBrightnessAfterLogin,
                     weak_ptr_factory_.GetWeakPtr()));
}

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

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

  SaveBrightnessPercentToLocalState(local_state_, active_account_id_.value(),
                                    brightness_percent.value());
}

void BrightnessControllerChromeos::OnGetHasAmbientLightSensor(
    std::optional<bool> has_sensor) {
  if (!has_sensor.has_value()) {
    LOG(ERROR)
        << "BrightnessControllerChromeos: Failed to get the ambient light "
           "sensor status";
    return;
  }
  has_sensor_ = has_sensor.value();
}

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

  // Save 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) {
    SaveBrightnessPercentToLocalState(local_state_, active_account_id_.value(),
                                      change.percent());
  }
}

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

  // If the 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).
  user_manager::KnownUser known_user(local_state_);
  if (!change.sensor_enabled()) {
    known_user.SetPath(
        active_account_id_.value(), prefs::kAmbientLightSensorDisabledReason,
        std::make_optional<base::Value>(static_cast<int>(change.cause())));
    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::kAmbientLightSensorDisabledReason);
  }

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

  if (ShouldSaveAmbientLightSensorForNewDevice(change)) {
    // Save ALS settings for new device if change is from settings app.
    PrefService* primary_user_prefs =
        session_controller_->GetActivePrefService();
    if (primary_user_prefs) {
      primary_user_prefs->SetBoolean(
          prefs::kDisplayAmbientLightSensorLastEnabled,
          change.sensor_enabled());
    }
  }
}

void BrightnessControllerChromeos::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.InHours() >= 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.Display.TimeUntilFirstBrightnessChange.",
                    is_on_login_screen ? "OnLoginScreen" : "AfterLogin", ".",
                    GetBrightnessActionName(brightness_action), "Brightness.",
                    IsChargerConnected() ? "Charger" : "Battery", "Power"}),
      time_since_last_session_change);
}

bool BrightnessControllerChromeos::IsInitialBrightnessSetByPolicy() {
  PrefService* pref_service = GetActivePrefService();
  if (!pref_service) {
    return false;
  }

  if (IsChargerConnected()) {
    return pref_service->IsManagedPreference(
        prefs::kPowerAcScreenBrightnessPercent);
  } else {
    return pref_service->IsManagedPreference(
        prefs::kPowerBatteryScreenBrightnessPercent);
  }
}

}  // namespace ash::system