blob: 5d90f01f88f5e40ab6e325bbc8c75e7ceaf378fb [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/device/battery/battery_status_manager_linux.h"
#include <stddef.h>
#include <stdint.h>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/message_loop/message_pump_type.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread.h"
#include "base/values.h"
#include "base/version.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "dbus/property.h"
#include "dbus/values_util.h"
#include "services/device/battery/battery_status_manager_linux-inl.h"
namespace device {
namespace {
const char kBatteryNotifierThreadName[] = "BatteryStatusNotifier";
class UPowerProperties : public dbus::PropertySet {
public:
UPowerProperties(dbus::ObjectProxy* object_proxy,
const PropertyChangedCallback callback);
UPowerProperties(const UPowerProperties&) = delete;
UPowerProperties& operator=(const UPowerProperties&) = delete;
~UPowerProperties() override;
base::Version daemon_version();
private:
dbus::Property<std::string> daemon_version_;
};
UPowerProperties::UPowerProperties(dbus::ObjectProxy* object_proxy,
const PropertyChangedCallback callback)
: dbus::PropertySet(object_proxy, kUPowerInterfaceName, callback) {
RegisterProperty(kUPowerPropertyDaemonVersion, &daemon_version_);
}
UPowerProperties::~UPowerProperties() {}
base::Version UPowerProperties::daemon_version() {
return (daemon_version_.is_valid() || daemon_version_.GetAndBlock())
? base::Version(daemon_version_.value())
: base::Version();
}
class UPowerObject {
public:
using PropertyChangedCallback = dbus::PropertySet::PropertyChangedCallback;
UPowerObject(dbus::Bus* dbus,
const PropertyChangedCallback property_changed_callback);
UPowerObject(const UPowerObject&) = delete;
UPowerObject& operator=(const UPowerObject&) = delete;
~UPowerObject();
std::vector<dbus::ObjectPath> EnumerateDevices();
dbus::ObjectPath GetDisplayDevice();
dbus::ObjectProxy* proxy() { return proxy_; }
UPowerProperties* properties() { return properties_.get(); }
private:
raw_ptr<dbus::Bus> dbus_; // Owned by the BatteryStatusNotificationThread.
raw_ptr<dbus::ObjectProxy> proxy_; // Owned by the dbus.
std::unique_ptr<UPowerProperties> properties_;
};
UPowerObject::UPowerObject(
dbus::Bus* dbus,
const PropertyChangedCallback property_changed_callback)
: dbus_(dbus),
proxy_(dbus_->GetObjectProxy(kUPowerServiceName,
dbus::ObjectPath(kUPowerPath))),
properties_(
std::make_unique<UPowerProperties>(proxy_,
property_changed_callback)) {}
UPowerObject::~UPowerObject() {
properties_.reset(); // before the proxy is deleted.
dbus_->RemoveObjectProxy(kUPowerServiceName, proxy_->object_path(),
base::DoNothing());
}
std::vector<dbus::ObjectPath> UPowerObject::EnumerateDevices() {
std::vector<dbus::ObjectPath> paths;
dbus::MethodCall method_call(kUPowerServiceName,
kUPowerMethodEnumerateDevices);
std::unique_ptr<dbus::Response> response(
proxy_
->CallMethodAndBlock(&method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)
.value_or(nullptr));
if (response) {
dbus::MessageReader reader(response.get());
reader.PopArrayOfObjectPaths(&paths);
}
return paths;
}
dbus::ObjectPath UPowerObject::GetDisplayDevice() {
dbus::ObjectPath display_device_path;
if (!proxy_)
return display_device_path;
dbus::MethodCall method_call(kUPowerServiceName,
kUPowerMethodGetDisplayDevice);
std::unique_ptr<dbus::Response> response(
proxy_
->CallMethodAndBlock(&method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)
.value_or(nullptr));
if (response) {
dbus::MessageReader reader(response.get());
reader.PopObjectPath(&display_device_path);
}
return display_device_path;
}
class BatteryProperties : public dbus::PropertySet {
public:
BatteryProperties(dbus::ObjectProxy* object_proxy,
const PropertyChangedCallback callback);
BatteryProperties(const BatteryProperties&) = delete;
BatteryProperties& operator=(const BatteryProperties&) = delete;
~BatteryProperties() override;
void ConnectSignals() override;
void Invalidate();
bool is_present(bool default_value = false);
double percentage(double default_value = 100);
uint32_t state(uint32_t default_value = UPOWER_DEVICE_STATE_UNKNOWN);
int64_t time_to_empty(int64_t default_value = 0);
int64_t time_to_full(int64_t default_value = 0);
uint32_t type(uint32_t default_value = UPOWER_DEVICE_TYPE_UNKNOWN);
private:
bool connected_ = false;
dbus::Property<bool> is_present_;
dbus::Property<double> percentage_;
dbus::Property<uint32_t> state_;
dbus::Property<int64_t> time_to_empty_;
dbus::Property<int64_t> time_to_full_;
dbus::Property<uint32_t> type_;
};
BatteryProperties::BatteryProperties(dbus::ObjectProxy* object_proxy,
const PropertyChangedCallback callback)
: dbus::PropertySet(object_proxy, kUPowerDeviceInterfaceName, callback) {
RegisterProperty(kUPowerDevicePropertyIsPresent, &is_present_);
RegisterProperty(kUPowerDevicePropertyPercentage, &percentage_);
RegisterProperty(kUPowerDevicePropertyState, &state_);
RegisterProperty(kUPowerDevicePropertyTimeToEmpty, &time_to_empty_);
RegisterProperty(kUPowerDevicePropertyTimeToFull, &time_to_full_);
RegisterProperty(kUPowerDevicePropertyType, &type_);
}
BatteryProperties::~BatteryProperties() {}
void BatteryProperties::ConnectSignals() {
if (!connected_) {
connected_ = true;
dbus::PropertySet::ConnectSignals();
}
}
void BatteryProperties::Invalidate() {
is_present_.set_valid(false);
percentage_.set_valid(false);
state_.set_valid(false);
time_to_empty_.set_valid(false);
time_to_full_.set_valid(false);
type_.set_valid(false);
}
bool BatteryProperties::is_present(bool default_value) {
return (is_present_.is_valid() || is_present_.GetAndBlock())
? is_present_.value()
: default_value;
}
double BatteryProperties::percentage(double default_value) {
return (percentage_.is_valid() || percentage_.GetAndBlock())
? percentage_.value()
: default_value;
}
uint32_t BatteryProperties::state(uint32_t default_value) {
return (state_.is_valid() || state_.GetAndBlock()) ? state_.value()
: default_value;
}
int64_t BatteryProperties::time_to_empty(int64_t default_value) {
return (time_to_empty_.is_valid() || time_to_empty_.GetAndBlock())
? time_to_empty_.value()
: default_value;
}
int64_t BatteryProperties::time_to_full(int64_t default_value) {
return (time_to_full_.is_valid() || time_to_full_.GetAndBlock())
? time_to_full_.value()
: default_value;
}
uint32_t BatteryProperties::type(uint32_t default_value) {
return (type_.is_valid() || type_.GetAndBlock()) ? type_.value()
: default_value;
}
class BatteryObject {
public:
using PropertyChangedCallback = dbus::PropertySet::PropertyChangedCallback;
BatteryObject(dbus::Bus* dbus,
const dbus::ObjectPath& device_path,
const PropertyChangedCallback& property_changed_callback);
BatteryObject(const BatteryObject&) = delete;
BatteryObject& operator=(const BatteryObject&) = delete;
~BatteryObject();
bool IsValid() const;
dbus::ObjectProxy* proxy() { return proxy_; }
BatteryProperties* properties() { return properties_.get(); }
private:
raw_ptr<dbus::Bus> dbus_; // Owned by the BatteryStatusNotificationThread,
raw_ptr<dbus::ObjectProxy> proxy_; // Owned by the dbus.
std::unique_ptr<BatteryProperties> properties_;
};
BatteryObject::BatteryObject(
dbus::Bus* dbus,
const dbus::ObjectPath& device_path,
const PropertyChangedCallback& property_changed_callback)
: dbus_(dbus),
proxy_(dbus_->GetObjectProxy(kUPowerServiceName, device_path)),
properties_(
std::make_unique<BatteryProperties>(proxy_,
property_changed_callback)) {}
BatteryObject::~BatteryObject() {
properties_.reset(); // before the proxy is deleted.
dbus_->RemoveObjectProxy(kUPowerServiceName, proxy_->object_path(),
base::DoNothing());
}
bool BatteryObject::IsValid() const {
return properties_->is_present() &&
properties_->type() == UPOWER_DEVICE_TYPE_BATTERY;
}
mojom::BatteryStatus ComputeWebBatteryStatus(BatteryProperties* properties) {
mojom::BatteryStatus status;
uint32_t state = properties->state();
status.charging = state != UPOWER_DEVICE_STATE_DISCHARGING &&
state != UPOWER_DEVICE_STATE_EMPTY;
// Convert percentage to a value between 0 and 1 with 2 digits of precision.
// This is to bring it in line with other platforms like Mac and Android where
// we report level with 1% granularity. It also serves the purpose of reducing
// the possibility of fingerprinting and triggers less level change events on
// the blink side.
// TODO(timvolodine): consider moving this rounding to the blink side.
status.level = round(properties->percentage()) / 100.f;
switch (state) {
case UPOWER_DEVICE_STATE_CHARGING: {
int64_t time_to_full = properties->time_to_full();
status.charging_time = (time_to_full > 0)
? time_to_full
: std::numeric_limits<double>::infinity();
break;
}
case UPOWER_DEVICE_STATE_DISCHARGING: {
int64_t time_to_empty = properties->time_to_empty();
// Set dischargingTime if it's available. Otherwise leave the default
// value which is +infinity.
if (time_to_empty > 0)
status.discharging_time = time_to_empty;
status.charging_time = std::numeric_limits<double>::infinity();
break;
}
case UPOWER_DEVICE_STATE_FULL: {
break;
}
default: {
status.charging_time = std::numeric_limits<double>::infinity();
}
}
return status;
}
} // namespace
// Class that represents a dedicated thread which communicates with DBus to
// obtain battery information and receives battery change notifications.
class BatteryStatusManagerLinux::BatteryStatusNotificationThread
: public base::Thread {
public:
explicit BatteryStatusNotificationThread(
const BatteryStatusService::BatteryUpdateCallback& callback)
: base::Thread(kBatteryNotifierThreadName), callback_(callback) {}
BatteryStatusNotificationThread(const BatteryStatusNotificationThread&) =
delete;
BatteryStatusNotificationThread& operator=(
const BatteryStatusNotificationThread&) = delete;
~BatteryStatusNotificationThread() override {
// Make sure to shutdown the dbus connection if it is still open in the very
// end. It needs to happen on the BatteryStatusNotificationThread.
task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&BatteryStatusNotificationThread::ShutdownDBusConnection,
base::Unretained(this)));
// Drain the message queue of the BatteryStatusNotificationThread and stop.
Stop();
}
void StartListening() {
DCHECK(OnWatcherThread());
if (upower_)
return;
if (!system_bus_)
InitDBus();
upower_ = std::make_unique<UPowerObject>(
system_bus_.get(), UPowerObject::PropertyChangedCallback());
upower_->proxy()->ConnectToSignal(
kUPowerServiceName, kUPowerSignalDeviceAdded,
base::BindRepeating(&BatteryStatusNotificationThread::DeviceAdded,
base::Unretained(this)),
base::DoNothing());
upower_->proxy()->ConnectToSignal(
kUPowerServiceName, kUPowerSignalDeviceRemoved,
base::BindRepeating(&BatteryStatusNotificationThread::DeviceRemoved,
base::Unretained(this)),
base::DoNothing());
FindBatteryDevice();
}
void StopListening() {
DCHECK(OnWatcherThread());
ShutdownDBusConnection();
}
void SetDBusForTesting(dbus::Bus* bus) { system_bus_ = bus; }
private:
bool OnWatcherThread() const {
return task_runner()->BelongsToCurrentThread();
}
void InitDBus() {
DCHECK(OnWatcherThread());
dbus::Bus::Options options;
options.bus_type = dbus::Bus::SYSTEM;
options.connection_type = dbus::Bus::PRIVATE;
system_bus_ = base::MakeRefCounted<dbus::Bus>(options);
}
bool IsDaemonVersionBelow_0_99() {
DCHECK(OnWatcherThread());
base::Version daemon_version = upower_->properties()->daemon_version();
return daemon_version.IsValid() &&
daemon_version.CompareTo(base::Version("0.99")) < 0;
}
void FindBatteryDevice() {
DCHECK(OnWatcherThread());
// If ShutdownDBusConnection() has been called, the DBus connection will be
// destroyed eventually. In the meanwhile, pending tasks can still end up
// calling this class. Ignore these calls.
if (!system_bus_)
return;
// Move the currently watched battery_ device to a stack-local variable such
// that we can enumerate all devices (once more):
// first testing the display device, then testing all devices from
// EnumerateDevices. We will monitor the first battery device we find.
// - That may be the same device we did monitor on entering this method;
// then we'll use the same BatteryObject instance, that was moved to
// current - see UseCurrentOrCreateBattery().
// - Or it may be a new device; then the previously monitored BatteryObject
// instance (if any) is released on leaving this function.
// - Or we may not find a battery device; then on leaving this function
// battery_ will be nullptr and the previously monitored BatteryObject
// instance (if any) is no longer a battery and will be released.
std::unique_ptr<BatteryObject> current = std::move(battery_);
auto UseCurrentOrCreateBattery =
[&current, this](const dbus::ObjectPath& device_path) {
if (current && current->proxy()->object_path() == device_path)
return std::move(current);
return CreateBattery(device_path);
};
dbus::ObjectPath display_device_path;
if (!IsDaemonVersionBelow_0_99())
display_device_path = upower_->GetDisplayDevice();
if (display_device_path.IsValid()) {
auto battery = UseCurrentOrCreateBattery(display_device_path);
if (battery->IsValid())
battery_ = std::move(battery);
}
if (!battery_) {
for (const auto& device_path : upower_->EnumerateDevices()) {
auto battery = UseCurrentOrCreateBattery(device_path);
if (!battery->IsValid())
continue;
if (battery_) {
// TODO(timvolodine): add support for multiple batteries. Currently we
// only collect information from the first battery we encounter
// (crbug.com/400780).
LOG(WARNING) << "multiple batteries found, "
<< "using status data of the first battery only.";
} else {
battery_ = std::move(battery);
}
}
}
if (!battery_) {
callback_.Run(mojom::BatteryStatus());
return;
}
battery_->properties()->ConnectSignals();
NotifyBatteryStatus();
if (IsDaemonVersionBelow_0_99()) {
// UPower Version 0.99 replaced the Changed signal with the
// PropertyChanged signal. For older versions we need to listen
// to the Changed signal.
battery_->proxy()->ConnectToSignal(
kUPowerDeviceInterfaceName, kUPowerDeviceSignalChanged,
base::BindRepeating(&BatteryStatusNotificationThread::BatteryChanged,
base::Unretained(this)),
base::DoNothing());
}
}
void ShutdownDBusConnection() {
DCHECK(OnWatcherThread());
if (!system_bus_)
return;
battery_.reset(); // before the |system_bus_| is shut down.
upower_.reset();
// Shutdown DBus connection later because there may be pending tasks on
// this thread.
task_runner()->PostTask(
FROM_HERE, base::BindOnce(&dbus::Bus::ShutdownAndBlock, system_bus_));
system_bus_ = nullptr;
}
std::unique_ptr<BatteryObject> CreateBattery(
const dbus::ObjectPath& device_path) {
return std::make_unique<BatteryObject>(
system_bus_.get(), device_path,
base::BindRepeating(
&BatteryStatusNotificationThread::BatteryPropertyChanged,
base::Unretained(this)));
}
void DeviceAdded(dbus::Signal* /* signal */) {
DCHECK(OnWatcherThread());
// Re-iterate all devices to see if we need to monitor the added battery
// instead of the currently monitored battery.
FindBatteryDevice();
}
void DeviceRemoved(dbus::Signal* signal) {
DCHECK(OnWatcherThread());
if (!battery_)
return;
// UPower specifies that the DeviceRemoved signal has an object-path as
// argument, however IRL that signal was observed with a string argument,
// so cover both cases (argument as string, as object-path and neither of
// these) and call FindBatteryDevice() if either we couldn't get the
// argument or the removed device-path is the battery_.
dbus::MessageReader reader(signal);
dbus::ObjectPath removed_device_path;
switch (reader.GetDataType()) {
case dbus::Message::DataType::STRING: {
std::string removed_device_path_string;
if (reader.PopString(&removed_device_path_string))
removed_device_path = dbus::ObjectPath(removed_device_path_string);
break;
}
case dbus::Message::DataType::OBJECT_PATH:
reader.PopObjectPath(&removed_device_path);
break;
default:
break;
}
if (!removed_device_path.IsValid() ||
battery_->proxy()->object_path() == removed_device_path) {
FindBatteryDevice();
}
}
void BatteryPropertyChanged(const std::string& /* property_name */) {
DCHECK(OnWatcherThread());
NotifyBatteryStatus();
}
void BatteryChanged(dbus::Signal* /* signal */) {
DCHECK(OnWatcherThread());
DCHECK(battery_);
battery_->properties()->Invalidate();
NotifyBatteryStatus();
}
void NotifyBatteryStatus() {
DCHECK(OnWatcherThread());
if (!system_bus_ || !battery_ || notifying_battery_status_)
return;
// If the system uses a UPower daemon older than version 0.99
// (see IsDaemonVersionBelow_0_99), then we are notified about changed
// battery_ properties through the 'Changed' signal of the battery_
// device (see BatteryChanged()). That is implemented to invalidate all
// battery_ properties (so they are re-fetched from the dbus). Getting
// the new property-value triggers a callback to BatteryPropertyChanged().
// notifying_battery_status_ is set to avoid recursion and computing the
// status too often.
notifying_battery_status_ = true;
callback_.Run(ComputeWebBatteryStatus(battery_->properties()));
notifying_battery_status_ = false;
}
const BatteryStatusService::BatteryUpdateCallback callback_;
scoped_refptr<dbus::Bus> system_bus_;
std::unique_ptr<UPowerObject> upower_;
std::unique_ptr<BatteryObject> battery_;
bool notifying_battery_status_ = false;
};
BatteryStatusManagerLinux::BatteryStatusManagerLinux(
const BatteryStatusService::BatteryUpdateCallback& callback)
: callback_(callback) {}
BatteryStatusManagerLinux::~BatteryStatusManagerLinux() {}
bool BatteryStatusManagerLinux::StartListeningBatteryChange() {
if (!StartNotifierThreadIfNecessary())
return false;
notifier_thread_->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&BatteryStatusNotificationThread::StartListening,
base::Unretained(notifier_thread_.get())));
return true;
}
void BatteryStatusManagerLinux::StopListeningBatteryChange() {
if (!notifier_thread_)
return;
notifier_thread_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&BatteryStatusNotificationThread::StopListening,
base::Unretained(notifier_thread_.get())));
}
bool BatteryStatusManagerLinux::StartNotifierThreadIfNecessary() {
if (notifier_thread_)
return true;
auto notifier_thread =
std::make_unique<BatteryStatusNotificationThread>(callback_);
if (!notifier_thread->StartWithOptions(
base::Thread::Options(base::MessagePumpType::IO, 0))) {
LOG(ERROR) << "Could not start the " << kBatteryNotifierThreadName
<< " thread";
return false;
}
notifier_thread_ = std::move(notifier_thread);
return true;
}
base::Thread* BatteryStatusManagerLinux::GetNotifierThreadForTesting() {
return notifier_thread_.get();
}
// static
std::unique_ptr<BatteryStatusManagerLinux>
BatteryStatusManagerLinux::CreateForTesting(
const BatteryStatusService::BatteryUpdateCallback& callback,
dbus::Bus* bus) {
auto manager = std::make_unique<BatteryStatusManagerLinux>(callback);
if (!manager->StartNotifierThreadIfNecessary())
return nullptr;
manager->notifier_thread_->SetDBusForTesting(bus);
return manager;
}
// static
std::unique_ptr<BatteryStatusManager> BatteryStatusManager::Create(
const BatteryStatusService::BatteryUpdateCallback& callback) {
return std::make_unique<BatteryStatusManagerLinux>(callback);
}
} // namespace device