blob: 21fd25278a6287b461fce6f856c56767f765e92c [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/power/smart_charging/smart_charging_manager.h"
#include <memory>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/task_runner_util.h"
#include "base/threading/scoped_blocking_call.h"
#include "chrome/browser/chromeos/power/ml/recent_events_counter.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chromeos/constants/devicetype.h"
#include "chromeos/dbus/power_manager/backlight.pb.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/core/session_manager_observer.h"
#include "components/viz/host/host_frame_sink_manager.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "ui/aura/env.h"
#include "ui/compositor/compositor.h"
namespace chromeos {
namespace power {
namespace {
constexpr int kBucketSize = 15;
// Interval at which data should be logged.
constexpr auto kLoggingInterval = base::TimeDelta::FromMinutes(30);
// Count number of key, mouse, touch events or duration of audio/video playing
// in the past 30 minutes.
constexpr auto kUserActivityDuration = base::TimeDelta::FromMinutes(30);
// Granularity of input events is per minute.
constexpr int kNumUserInputEventsBuckets = kUserActivityDuration.InMinutes();
constexpr char kSavedFileName[] = "past_charging_events.pb";
constexpr char kSavedDir[] = "smartcharging";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class SmartChargingMessage {
kSerializeProtoError = 0,
kWriteFileError = 1,
kWriteFileSuccess = 2,
kReadFileError = 3,
kParseProtoError = 4,
kReadFileSuccess = 5,
kGetPrimaryProfileError = 6,
kCreateFolderError = 7,
kCreatePathSuccess = 8,
kPathDoesntExists = 9,
kGetPathSuccess = 10,
kMaxValue = kGetPathSuccess
};
void LogSmartChargingMessage(SmartChargingMessage message) {
UMA_HISTOGRAM_ENUMERATION("Power.SmartCharging.Messages", message);
}
// Given a proto and file path, writes to disk and logs the error(if any).
void WriteProtoToDisk(const PastChargingEvents& proto,
const base::FilePath& file_path) {
std::string proto_string;
if (!proto.SerializeToString(&proto_string)) {
LogSmartChargingMessage(SmartChargingMessage::kSerializeProtoError);
return;
}
bool write_result;
{
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
write_result = base::ImportantFileWriter::WriteFileAtomically(
file_path, proto_string.data(), "SmartCharging");
}
if (!write_result) {
LogSmartChargingMessage(SmartChargingMessage::kWriteFileError);
return;
}
LogSmartChargingMessage(SmartChargingMessage::kWriteFileSuccess);
}
// Reads a proto from a file path.
std::unique_ptr<PastChargingEvents> ReadProto(const base::FilePath& file_path) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
std::string proto_str;
if (!base::ReadFileToString(file_path, &proto_str)) {
LogSmartChargingMessage(SmartChargingMessage::kReadFileError);
return nullptr;
}
auto proto = std::make_unique<PastChargingEvents>();
if (!proto->ParseFromString(proto_str)) {
LogSmartChargingMessage(SmartChargingMessage::kParseProtoError);
return nullptr;
}
LogSmartChargingMessage(SmartChargingMessage::kReadFileSuccess);
return proto;
}
// If |create_new_path| is true, try to create new path and return true if
// success.
// If |create_new_path| is false, try to get the path and return true if
// sucesss.
bool GetPathSuccess(const base::FilePath profile_path,
base::FilePath* file_path,
bool create_new_path) {
const base::FilePath path = profile_path.AppendASCII(kSavedDir);
if (create_new_path) {
if (!base::DirectoryExists(path) && !base::CreateDirectory(path)) {
LogSmartChargingMessage(SmartChargingMessage::kCreateFolderError);
return false;
}
} else if (!base::PathExists(path.AppendASCII(kSavedFileName))) {
LogSmartChargingMessage(SmartChargingMessage::kPathDoesntExists);
return false;
}
*file_path = path.AppendASCII(kSavedFileName);
LogSmartChargingMessage(SmartChargingMessage::kGetPathSuccess);
return true;
}
// Checks if an event is a halt (shutdown/suspend) event.
bool IsHaltEvent(const PastEvent& event) {
return event.reason() == UserChargingEvent::Event::SHUTDOWN ||
event.reason() == UserChargingEvent::Event::SUSPEND;
}
// Loads data from disk given a profile file path.
std::unique_ptr<PastChargingEvents> LoadFromDisk(
const base::FilePath& profile_path) {
base::FilePath file_path;
std::unique_ptr<PastChargingEvents> proto;
if (GetPathSuccess(profile_path, &file_path, false /*create_new_path*/)) {
proto = ReadProto(file_path);
}
return proto;
}
// Saves data to disk given a profile file path.
void SaveToDisk(const std::vector<PastEvent>& past_events,
const base::FilePath& profile_path) {
base::FilePath file_path;
if (GetPathSuccess(profile_path, &file_path, true /*create_new_path*/)) {
PastChargingEvents proto;
for (const auto& event : past_events) {
*proto.add_events() = event;
}
WriteProtoToDisk(proto, file_path);
}
}
} // namespace
SmartChargingManager::SmartChargingManager(
ui::UserActivityDetector* detector,
mojo::PendingReceiver<viz::mojom::VideoDetectorObserver> receiver,
session_manager::SessionManager* session_manager,
std::unique_ptr<base::RepeatingTimer> periodic_timer)
: periodic_timer_(std::move(periodic_timer)),
receiver_(this, std::move(receiver)),
mouse_counter_(std::make_unique<ml::RecentEventsCounter>(
kUserActivityDuration,
kNumUserInputEventsBuckets)),
key_counter_(std::make_unique<ml::RecentEventsCounter>(
kUserActivityDuration,
kNumUserInputEventsBuckets)),
stylus_counter_(std::make_unique<ml::RecentEventsCounter>(
kUserActivityDuration,
kNumUserInputEventsBuckets)),
touch_counter_(std::make_unique<ml::RecentEventsCounter>(
kUserActivityDuration,
kNumUserInputEventsBuckets)),
ukm_logger_(std::make_unique<SmartChargingUkmLogger>()) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(detector);
DCHECK(session_manager);
user_activity_observer_.Add(detector);
power_manager_client_observer_.Add(chromeos::PowerManagerClient::Get());
session_manager_observer_.Add(session_manager);
blocking_task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
}
SmartChargingManager::~SmartChargingManager() = default;
std::unique_ptr<SmartChargingManager> SmartChargingManager::CreateInstance() {
// TODO(crbug.com/1028853): we are collecting data from Chromebook only. Since
// this action is discouraged, we will modify the condition latter using dbus
// calls.
if (chromeos::GetDeviceType() != chromeos::DeviceType::kChromebook)
return nullptr;
ui::UserActivityDetector* const detector = ui::UserActivityDetector::Get();
DCHECK(detector);
mojo::PendingRemote<viz::mojom::VideoDetectorObserver> video_observer;
std::unique_ptr<SmartChargingManager> screen_brightness_manager =
std::make_unique<SmartChargingManager>(
detector, video_observer.InitWithNewPipeAndPassReceiver(),
session_manager::SessionManager::Get(),
std::make_unique<base::RepeatingTimer>());
aura::Env::GetInstance()
->context_factory()
->GetHostFrameSinkManager()
->AddVideoDetectorObserver(std::move(video_observer));
return screen_brightness_manager;
}
void SmartChargingManager::OnUserActivity(const ui::Event* event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!event)
return;
const base::TimeDelta time_since_boot = boot_clock_.GetTimeSinceBoot();
// Using time_since_boot instead of the event's time stamp so we can use the
// boot clock.
if (event->IsMouseEvent()) {
mouse_counter_->Log(time_since_boot);
return;
}
if (event->IsKeyEvent()) {
key_counter_->Log(time_since_boot);
return;
}
if (event->IsTouchEvent()) {
if (event->AsTouchEvent()->pointer_details().pointer_type ==
ui::EventPointerType::kPen) {
stylus_counter_->Log(time_since_boot);
return;
}
touch_counter_->Log(time_since_boot);
}
}
void SmartChargingManager::ScreenBrightnessChanged(
const power_manager::BacklightBrightnessChange& change) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
screen_brightness_percent_ = change.percent();
}
void SmartChargingManager::PowerChanged(
const power_manager::PowerSupplyProperties& proto) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (proto.has_battery_percent()) {
battery_percent_ = proto.battery_percent();
}
if (!proto.has_external_power()) {
return;
}
// Logic for the first PowerChanged call.
if (!external_power_.has_value()) {
external_power_ = proto.external_power();
periodic_timer_->Start(FROM_HERE, kLoggingInterval, this,
&SmartChargingManager::OnTimerFired);
if (external_power_.value() == power_manager::PowerSupplyProperties::AC) {
is_charging_ = true;
LogEvent(UserChargingEvent::Event::CHARGER_PLUGGED_IN);
} else {
is_charging_ = false;
}
return;
}
const bool was_on_ac =
external_power_.value() == power_manager::PowerSupplyProperties::AC;
const bool now_on_ac =
proto.external_power() == power_manager::PowerSupplyProperties::AC;
is_charging_ = now_on_ac;
// User plugged the charger in.
if (!was_on_ac && now_on_ac) {
external_power_ = proto.external_power();
LogEvent(UserChargingEvent::Event::CHARGER_PLUGGED_IN);
} else if (was_on_ac && !now_on_ac) {
// User unplugged the charger.
external_power_ = proto.external_power();
LogEvent(UserChargingEvent::Event::CHARGER_UNPLUGGED);
}
}
void SmartChargingManager::PowerManagerBecameAvailable(bool available) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!available) {
return;
}
chromeos::PowerManagerClient::Get()->RequestStatusUpdate();
chromeos::PowerManagerClient::Get()->GetScreenBrightnessPercent(
base::BindOnce(&SmartChargingManager::OnReceiveScreenBrightnessPercent,
weak_ptr_factory_.GetWeakPtr()));
chromeos::PowerManagerClient::Get()->GetSwitchStates(
base::BindOnce(&SmartChargingManager::OnReceiveSwitchStates,
weak_ptr_factory_.GetWeakPtr()));
}
void SmartChargingManager::ShutdownRequested(
power_manager::RequestShutdownReason reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LogEvent(UserChargingEvent::Event::SHUTDOWN);
}
void SmartChargingManager::SuspendImminent(
power_manager::SuspendImminent::Reason reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LogEvent(UserChargingEvent::Event::SUSPEND);
}
void SmartChargingManager::LidEventReceived(
const chromeos::PowerManagerClient::LidState state,
base::TimeTicks /* timestamp */) {
lid_state_ = state;
}
void SmartChargingManager::TabletModeEventReceived(
const chromeos::PowerManagerClient::TabletMode mode,
base::TimeTicks /* timestamp */) {
tablet_mode_ = mode;
}
void SmartChargingManager::OnVideoActivityStarted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
most_recent_video_start_time_ = boot_clock_.GetTimeSinceBoot();
is_video_playing_ = true;
}
void SmartChargingManager::OnVideoActivityEnded() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
recent_video_usage_.push_back(TimePeriod(most_recent_video_start_time_,
boot_clock_.GetTimeSinceBoot()));
is_video_playing_ = false;
}
void SmartChargingManager::OnUserSessionStarted(bool /* is_primary_user */) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The first sign-in user is the primary user, hence if |OnUserSessionStarted|
// is called, the primary user profile should have been created. We will
// ignore |is_primary_user|.
if (loaded_from_disk_)
return;
if (!ProfileManager::GetPrimaryUserProfile()) {
LogSmartChargingMessage(SmartChargingMessage::kGetPrimaryProfileError);
return;
}
profile_path_ = ProfileManager::GetPrimaryUserProfile()->GetPath();
MaybeLoadFromDisk(profile_path_.value());
}
void SmartChargingManager::PopulateUserChargingEventProto(
UserChargingEvent* proto) {
auto& features = *proto->mutable_features();
if (battery_percent_)
features.set_battery_percentage(static_cast<int>(battery_percent_.value()));
const base::TimeDelta time_since_boot = boot_clock_.GetTimeSinceBoot();
features.set_num_recent_key_events(key_counter_->GetTotal(time_since_boot));
features.set_num_recent_mouse_events(
mouse_counter_->GetTotal(time_since_boot));
features.set_num_recent_touch_events(
touch_counter_->GetTotal(time_since_boot));
features.set_num_recent_stylus_events(
stylus_counter_->GetTotal(time_since_boot));
if (is_charging_.has_value())
features.set_is_charging(is_charging_.value());
if (screen_brightness_percent_)
features.set_screen_brightness_percent(
static_cast<int>(screen_brightness_percent_.value()));
features.set_duration_recent_video_playing(
ukm::GetExponentialBucketMinForUserTiming(
DurationRecentVideoPlaying().InMinutes()));
// Set time related features.
const base::Time now = base::Time::Now();
base::Time::Exploded now_exploded;
now.LocalExplode(&now_exploded);
features.set_time_of_the_day(ukm::GetLinearBucketMin(
static_cast<int64_t>(now_exploded.hour * 60 + now_exploded.minute),
kBucketSize));
features.set_day_of_week(static_cast<UserChargingEvent::Features::DayOfWeek>(
now_exploded.day_of_week));
features.set_day_of_month(now_exploded.day_of_month);
features.set_month(
static_cast<UserChargingEvent::Features::Month>(now_exploded.month));
// Set device mode.
if (lid_state_ == chromeos::PowerManagerClient::LidState::CLOSED) {
features.set_device_mode(UserChargingEvent::Features::CLOSED_LID_MODE);
} else if (tablet_mode_ == chromeos::PowerManagerClient::TabletMode::ON) {
features.set_device_mode(UserChargingEvent::Features::TABLET_MODE);
} else if (lid_state_ == chromeos::PowerManagerClient::LidState::OPEN) {
features.set_device_mode(UserChargingEvent::Features::LAPTOP_MODE);
} else {
features.set_device_mode(UserChargingEvent::Features::UNKNOWN_MODE);
}
// Last charge related features. This logic relies on the fact that
// there will be at most one halt event because of |UpdatePastEvents()|.
bool halt_from_last_charge = false;
for (const auto& event : past_events_) {
if (IsHaltEvent(event)) {
halt_from_last_charge = true;
break;
}
}
features.set_halt_from_last_charge(halt_from_last_charge);
PastEvent last_charge_plugged_in;
PastEvent last_charge_unplugged;
std::tie(last_charge_plugged_in, last_charge_unplugged) =
GetLastChargeEvents();
if (!last_charge_plugged_in.has_time() || !last_charge_unplugged.has_time())
return;
features.set_time_since_last_charge(ukm::GetExponentialBucketMinForCounts1000(
now.ToDeltaSinceWindowsEpoch().InMinutes() -
last_charge_unplugged.time()));
features.set_duration_of_last_charge(
ukm::GetExponentialBucketMinForCounts1000(last_charge_unplugged.time() -
last_charge_plugged_in.time()));
features.set_battery_percentage_before_last_charge(
last_charge_plugged_in.battery_percent());
features.set_battery_percentage_of_last_charge(
last_charge_unplugged.battery_percent());
features.set_timezone_difference_from_last_charge(
(now.UTCMidnight() - now.LocalMidnight()).InHours() -
last_charge_unplugged.timezone());
}
void SmartChargingManager::LogEvent(const EventReason& reason) {
UserChargingEvent proto;
proto.mutable_event()->set_event_id(++event_id_);
proto.mutable_event()->set_reason(reason);
PopulateUserChargingEventProto(&proto);
// TODO(crbug.com/1028853): This is for testing only. Need to remove when
// ukm logger is available.
user_charging_event_for_test_ = proto;
ukm_logger_->LogEvent(proto);
AddPastEvent(reason);
// Calls |UpdatePastEvents()| after |AddPastEvent()| to keep the number of
// saved past events to be minimum.
UpdatePastEvents();
if (profile_path_.has_value()) {
MaybeSaveToDisk(profile_path_.value());
} else {
LogSmartChargingMessage(SmartChargingMessage::kGetPrimaryProfileError);
}
}
void SmartChargingManager::OnTimerFired() {
LogEvent(UserChargingEvent_Event::PERIODIC_LOG);
}
void SmartChargingManager::OnReceiveScreenBrightnessPercent(
base::Optional<double> screen_brightness_percent) {
if (screen_brightness_percent.has_value()) {
screen_brightness_percent_ = *screen_brightness_percent;
}
}
void SmartChargingManager::OnReceiveSwitchStates(
const base::Optional<chromeos::PowerManagerClient::SwitchStates>
switch_states) {
if (switch_states.has_value()) {
lid_state_ = switch_states->lid_state;
tablet_mode_ = switch_states->tablet_mode;
}
}
base::TimeDelta SmartChargingManager::DurationRecentVideoPlaying() {
// Removes events that is out of |kUserActivityDuration|.
const base::TimeDelta start_of_duration =
boot_clock_.GetTimeSinceBoot() - kUserActivityDuration;
while (!recent_video_usage_.empty() &&
recent_video_usage_.front().end_time < start_of_duration) {
recent_video_usage_.pop_front();
}
// Calculates total time.
base::TimeDelta total_time = base::TimeDelta::FromSeconds(0);
for (const auto& event : recent_video_usage_) {
total_time += std::min(event.end_time - event.start_time,
event.end_time - start_of_duration);
}
if (is_video_playing_) {
total_time +=
std::min(kUserActivityDuration, boot_clock_.GetTimeSinceBoot() -
most_recent_video_start_time_);
}
return total_time;
}
void SmartChargingManager::MaybeLoadFromDisk(
const base::FilePath& profile_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(), FROM_HERE,
base::BindOnce(&LoadFromDisk, profile_path),
base::BindOnce(&SmartChargingManager::OnLoadProtoFromDiskComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void SmartChargingManager::MaybeSaveToDisk(const base::FilePath& profile_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
blocking_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&SaveToDisk, past_events_, profile_path));
}
void SmartChargingManager::OnLoadProtoFromDiskComplete(
std::unique_ptr<PastChargingEvents> proto) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!proto) {
return;
}
loaded_from_disk_ = true;
for (const auto& event : proto.get()->events()) {
past_events_.emplace_back(event);
}
}
void SmartChargingManager::AddPastEvent(const EventReason& reason) {
// Since we use |past_events_| to calculate last charge information, we don't
// need to save if the reason is PERIODIC_LOG.
if (reason == UserChargingEvent::Event::PERIODIC_LOG)
return;
PastEvent new_event;
const base::Time now = base::Time::Now();
new_event.set_time(now.ToDeltaSinceWindowsEpoch().InMinutes());
if (battery_percent_.has_value())
new_event.set_battery_percent(static_cast<int>(battery_percent_.value()));
new_event.set_timezone((now.UTCMidnight() - now.LocalMidnight()).InHours());
new_event.set_reason(reason);
past_events_.emplace_back(new_event);
}
void SmartChargingManager::UpdatePastEvents() {
PastEvent last_charge_plugged_in;
PastEvent last_charge_unplugged;
PastEvent new_plugged_in;
PastEvent new_halt;
std::tie(last_charge_plugged_in, last_charge_unplugged) =
GetLastChargeEvents();
if (last_charge_unplugged.has_time()) {
// Gets the unplugged and halt(shutdown/suspend) events after the unplug (if
// any).
for (const auto& event : past_events_) {
if (event.time() > last_charge_unplugged.time()) {
if (event.reason() == UserChargingEvent::Event::CHARGER_PLUGGED_IN) {
new_plugged_in = event;
} else if (IsHaltEvent(event)) {
new_halt = event;
}
}
}
} else {
// Gets the last halt and plugged in event.
for (const auto& event : past_events_) {
if (event.reason() == UserChargingEvent::Event::CHARGER_PLUGGED_IN) {
new_plugged_in = event;
}
if (IsHaltEvent(event)) {
new_halt = event;
}
}
}
// Removes everything else.
past_events_.clear();
// Adds useful events back.
if (last_charge_plugged_in.has_time())
past_events_.emplace_back(last_charge_plugged_in);
if (last_charge_unplugged.has_time())
past_events_.emplace_back(last_charge_unplugged);
if (new_plugged_in.has_time())
past_events_.emplace_back(new_plugged_in);
if (new_halt.has_time())
past_events_.emplace_back(new_halt);
}
// Returns the last pair of plug/unplug events. If can't find the last pair,
// return a pair of empty events.
std::tuple<PastEvent, PastEvent> SmartChargingManager::GetLastChargeEvents() {
PastEvent plugged_in;
PastEvent unplugged;
PastEvent temp_plugged_in;
// There could be multiple events with CHARGER_PLUGGED_IN and/or
// CHARGER_UNPLUGGED. This function relies on the fact that all events are
// sorted by time.
for (const auto& event : past_events_) {
if (event.has_reason()) {
if (event.reason() == UserChargingEvent::Event::CHARGER_PLUGGED_IN) {
temp_plugged_in = event;
} else if (event.reason() ==
UserChargingEvent::Event::CHARGER_UNPLUGGED) {
if (!temp_plugged_in.has_time())
continue;
// Updates the pair of results.
if (!plugged_in.has_time() ||
temp_plugged_in.time() != plugged_in.time()) {
plugged_in = temp_plugged_in;
unplugged = event;
}
}
}
}
return std::make_tuple(plugged_in, unplugged);
}
} // namespace power
} // namespace chromeos