blob: de3485bcd0b7477a7e95bf5878725aa773f5d63e [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "power_manager/powerd/policy/bluetooth_controller.h"
#include <string>
#include <utility>
#include <vector>
#include <base/check.h>
#include <base/check_op.h>
#include <base/containers/contains.h>
#include <base/files/file_util.h>
#include <base/functional/bind.h>
#include <base/logging.h>
#include <featured/feature_library.h>
namespace power_manager::policy {
namespace {
const char kFlossManagerService[] = "org.chromium.bluetooth.Manager";
const char kFlossManagerInterface[] = "org.chromium.bluetooth.Manager";
const char kFlossManagerObject[] = "/org/chromium/bluetooth/Manager";
const char kSetTabletMode[] = "SetTabletMode";
constexpr base::TimeDelta kFlossManagerDBusTimeout = base::Seconds(2);
// Check if the path exists at the wakeup path and return the
// path to the power/control path if it does.
base::FilePath GetSysattrPathFromWakeupDevicePath(
const std::string& sysattr_fragment,
const base::FilePath& wakeup_device_path,
const std::string& syspath) {
base::FilePath control_path = wakeup_device_path.Append(sysattr_fragment);
if (!base::PathExists(control_path)) {
return base::FilePath();
}
if (!base::PathIsReadable(control_path) ||
!base::PathIsWritable(control_path)) {
LOG(ERROR) << "Bluetooth device " << sysattr_fragment
<< " is not accessible to powerd: " << control_path
<< ", syspath=" << syspath;
}
return control_path;
}
base::FilePath GetControlPathFromWakeupDevicePath(
const base::FilePath& wakeup_device_path, const std::string& syspath) {
return GetSysattrPathFromWakeupDevicePath(
BluetoothController::kAutosuspendSysattr, wakeup_device_path, syspath);
}
base::FilePath GetDelayPathFromWakeupDevicePath(
const base::FilePath& wakeup_device_path, const std::string& syspath) {
return GetSysattrPathFromWakeupDevicePath(
BluetoothController::kAutosuspendDelaySysattr, wakeup_device_path,
syspath);
}
bool SetPathValue(const base::FilePath& path, const std::string& contents) {
bool success = base::WriteFile(path, contents);
LOG(INFO) << "Writing \"" << contents << "\" to " << path << " "
<< (success ? "succeeded" : "failed");
return success;
}
} // namespace
const char BluetoothController::kUdevSubsystemBluetooth[] = "bluetooth";
const char BluetoothController::kUdevDevtypeHost[] = "host";
const char BluetoothController::kUdevSubsystemInput[] = "input";
// See https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power
const char BluetoothController::kAutosuspendSysattr[] = "power/control";
const char BluetoothController::kAutosuspendDelaySysattr[] =
"power/autosuspend_delay_ms";
const char BluetoothController::kAutosuspendEnabled[] = "auto";
const char BluetoothController::kAutosuspendDisabled[] = "on";
const char BluetoothController::kBluetoothInputRole[] = "cros_bluetooth";
// Long timeout is 2 minutes.
const char BluetoothController::kLongAutosuspendTimeout[] = "120000";
// Default timeout is 2s (Linux standard).
const char BluetoothController::kDefaultAutosuspendTimeout[] = "2000";
// Long autosuspend feature.
const char BluetoothController::kLongAutosuspendFeatureName[] =
"CrOSLateBootLongBluetoothAutosuspend";
const VariationsFeature kLongAutosuspendFeature{
BluetoothController::kLongAutosuspendFeatureName,
FEATURE_ENABLED_BY_DEFAULT};
BluetoothController::BluetoothController() = default;
BluetoothController::~BluetoothController() {
if (udev_) {
udev_->RemoveSubsystemObserver(kUdevSubsystemBluetooth, this);
udev_->RemoveTaggedDeviceObserver(this);
}
if (dbus_wrapper_) {
dbus_wrapper_->RemoveObserver(this);
}
}
void BluetoothController::Init(
system::UdevInterface* udev,
feature::PlatformFeaturesInterface* platform_features,
system::DBusWrapperInterface* dbus_wrapper,
TabletMode tablet_mode) {
DCHECK(udev);
DCHECK(platform_features);
DCHECK(dbus_wrapper);
udev_ = udev;
platform_features_ = platform_features;
tablet_mode_ = tablet_mode;
dbus_wrapper_ = dbus_wrapper;
dbus_wrapper_->AddObserver(this);
floss_dbus_proxy_ =
dbus_wrapper->GetObjectProxy(kFlossManagerService, kFlossManagerObject);
dbus_wrapper->RegisterForServiceAvailability(
floss_dbus_proxy_,
base::BindOnce(
&BluetoothController::HandleFlossServiceAvailableOrRestarted,
weak_ptr_factory_.GetWeakPtr()));
// Register for udev updates.
udev_->AddSubsystemObserver(kUdevSubsystemBluetooth, this);
udev_->AddTaggedDeviceObserver(this);
// List all initial entries in Bluetooth subsystem.
bt_hosts_.clear();
std::vector<system::UdevDeviceInfo> found;
if (udev_->GetSubsystemDevices(kUdevSubsystemBluetooth, &found)) {
for (const system::UdevDeviceInfo& dev : found) {
if (dev.devtype != kUdevDevtypeHost) {
continue;
}
base::FilePath control_path = GetControlPathFromWakeupDevicePath(
dev.wakeup_device_path, dev.syspath);
bt_hosts_.emplace(std::make_pair(dev.syspath, control_path));
}
}
// Register for refetch with platform_features. The check for bus is just for
// tests which don't have a bus assigned.
if (dbus_wrapper->GetBus() != nullptr) {
platform_features_->ListenForRefetchNeeded(
base::BindRepeating(&BluetoothController::RefetchFeatures,
weak_ptr_factory_.GetWeakPtr()),
base::DoNothing());
}
// Do the initial fetch.
RefetchFeatures();
}
// Inform tablet mode change through dbus
void BluetoothController::DBusInformTabletModeChange() {
bool tablet_mode_enabled = false;
switch (tablet_mode_) {
case TabletMode::ON:
tablet_mode_enabled = true;
break;
case TabletMode::OFF:
case TabletMode::UNSUPPORTED:
tablet_mode_enabled = false;
break;
default:
LOG(ERROR) << "Undefined tablet mode.";
}
if (!dbus_wrapper_ || !floss_dbus_proxy_) {
return;
}
dbus::MethodCall method_call(kFlossManagerInterface, kSetTabletMode);
dbus::MessageWriter(&method_call).AppendBool(tablet_mode_enabled);
dbus_wrapper_->CallMethodAsync(
floss_dbus_proxy_, &method_call, kFlossManagerDBusTimeout,
base::BindRepeating(&BluetoothController::SetTabletModeResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void BluetoothController::SetTabletModeResponse(dbus::Response* response) {
if (!response) {
LOG(ERROR) << "D-Bus method call to " << kFlossManagerInterface << "."
<< kSetTabletMode << " failed";
return;
}
}
void BluetoothController::HandleTabletModeChange(TabletMode mode) {
if (tablet_mode_ == mode) {
return;
}
tablet_mode_ = mode;
DBusInformTabletModeChange();
}
void BluetoothController::OnDBusNameOwnerChanged(
const std::string& service_name,
const std::string& old_owner,
const std::string& new_owner) {
// When Floss restarts.
if (service_name == kFlossManagerService && !new_owner.empty()) {
LOG(INFO) << "D-Bus " << service_name << " ownership changed to "
<< new_owner;
HandleFlossServiceAvailableOrRestarted(true);
}
}
void BluetoothController::HandleFlossServiceAvailableOrRestarted(
bool available) {
if (!available) {
LOG(ERROR) << "Failed waiting for Floss to become available";
return;
}
DBusInformTabletModeChange();
}
void BluetoothController::ApplyAutosuspendQuirk() {
std::string disable(kAutosuspendDisabled);
for (const auto& device : bt_hosts_) {
// If the host device has a power/control sysattr, disable autosuspend
// before we enter suspend.
if (device.second != base::FilePath()) {
std::string current_value;
// Save previous state.
if (base::ReadFileToString(device.second, &current_value)) {
autosuspend_state_before_quirks_[device.second] = current_value;
}
if (current_value == disable) {
return;
}
SetPathValue(device.second, disable);
}
}
}
void BluetoothController::UnapplyAutosuspendQuirk() {
// Default the restore action to enabling autosuspend.
std::string restore(kAutosuspendEnabled);
for (const auto& device : bt_hosts_) {
if (device.second != base::FilePath()) {
std::string current_value;
// Restore the state of autosuspend before quirks were applied.
if (base::Contains(autosuspend_state_before_quirks_, device.second)) {
restore = autosuspend_state_before_quirks_[device.second];
}
if (base::ReadFileToString(device.second, &current_value)) {
if (current_value == restore) {
return;
}
}
SetPathValue(device.second, restore);
}
}
// Clear previous autosuspend quirks state.
autosuspend_state_before_quirks_.clear();
}
void BluetoothController::OnTaggedDeviceChanged(
const system::TaggedDevice& device) {
// Feature should be enabled for us to handle device changes.
if (!long_autosuspend_feature_enabled_) {
return;
}
// We only care about devices with the cros_bluetooth tag.
if (!udev_->HasPowerdRole(device.syspath(), kBluetoothInputRole)) {
return;
}
// Device is already in connected list.
if (base::Contains(connected_bluetooth_input_devices_, device.syspath())) {
return;
}
base::FilePath delay_path = GetDelayPathFromWakeupDevicePath(
device.wakeup_device_path(), device.syspath());
connected_bluetooth_input_devices_.emplace(
std::make_pair(device.syspath(), delay_path));
// If this is the first instance of a connected device for this path, set the
// long autosuspend delay and set the count.
if (!base::Contains(delay_path_connected_count_, delay_path)) {
std::string long_autosuspend(kLongAutosuspendTimeout);
delay_path_connected_count_.emplace(std::make_pair(delay_path, 1));
SetPathValue(delay_path, long_autosuspend);
} else {
delay_path_connected_count_[delay_path]++;
}
}
void BluetoothController::OnTaggedDeviceRemoved(
const system::TaggedDevice& device) {
// Ignore unknown devices.
if (!base::Contains(connected_bluetooth_input_devices_, device.syspath())) {
return;
}
// Note: We intentionally do not guard against the feature flag. If any
// devices were added to |connected_bluetooth_input_devices_|, they should be
// removed to restore the original system state.
std::string default_autosuspend_delay(kDefaultAutosuspendTimeout);
// Remove this device from our connected list.
base::FilePath delay_path =
connected_bluetooth_input_devices_[device.syspath()];
connected_bluetooth_input_devices_.erase(device.syspath());
if (base::Contains(delay_path_connected_count_, delay_path)) {
delay_path_connected_count_[delay_path]--;
// If this is the final connection, reset autosuspend delay.
if (delay_path_connected_count_[delay_path] == 0) {
SetPathValue(delay_path, default_autosuspend_delay);
delay_path_connected_count_.erase(delay_path);
}
} else {
// This path shouldn't happen but we should restore to normal delay.
VLOG(1) << "Known Bluetooth tagged device removed but connected count "
"didn't exist.";
SetPathValue(delay_path, default_autosuspend_delay);
}
}
void BluetoothController::OnUdevEvent(const system::UdevEvent& event) {
DCHECK_EQ(event.device_info.subsystem, kUdevSubsystemBluetooth);
if (event.device_info.devtype != kUdevDevtypeHost) {
return;
}
base::FilePath control_path;
// Update the power/control path when bluetooth hosts are added or removed.
switch (event.action) {
case system::UdevEvent::Action::ADD:
case system::UdevEvent::Action::CHANGE:
control_path = GetControlPathFromWakeupDevicePath(
event.device_info.wakeup_device_path, event.device_info.syspath);
bt_hosts_.emplace(
std::make_pair(event.device_info.syspath, control_path));
break;
case system::UdevEvent::Action::REMOVE:
bt_hosts_.erase(base::FilePath(event.device_info.syspath));
break;
default:
break;
}
}
void BluetoothController::RefetchFeatures() {
EnableLongAutosuspendFeature(
platform_features_->IsEnabledBlocking(kLongAutosuspendFeature));
}
void BluetoothController::EnableLongAutosuspendFeature(bool enable) {
long_autosuspend_feature_enabled_ = enable;
// List all initially tagged devices and update the notification.
for (const system::TaggedDevice& device : udev_->GetTaggedDevices()) {
if (long_autosuspend_feature_enabled_) {
OnTaggedDeviceChanged(device);
} else {
OnTaggedDeviceRemoved(device);
}
}
// Log the change in status for this feature.
LOG(INFO) << "Bluetooth long autosuspend feature is "
<< (long_autosuspend_feature_enabled_ ? "enabled" : "disabled");
}
} // namespace power_manager::policy