blob: ac5db862d50c400d3d792a8c6b30bfbdef7d2367 [file] [log] [blame]
// Copyright 2020 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/power/peripheral_battery_listener.h"
#include <optional>
#include <string>
#include <vector>
#include "ash/power/hid_battery_util.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/stylus_state.h"
#include "ui/events/devices/touchscreen_device.h"
#include "ui/gfx/image/image.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/ozone/public/input_controller.h"
#include "ui/ozone/public/ozone_platform.h"
namespace ash {
namespace {
constexpr char kBluetoothDeviceIdPrefix[] = "battery_bluetooth-";
// Currently we expect at most one peripheral charger to exist, and
// it will always be the stylus charger.
// TODO(b/215381232): Temporarily support both PCHG name and peripheral name
// till upstream kernel driver is merged.
constexpr LazyRE2 kPeripheralChargerRegex = {
R"(/(?:peripheral|PCHG)(?:[0-9]+)$)"};
constexpr char kStylusChargerID[] = "peripheral0";
// TODO(b/187298772,b/187299765): if we have docked stylus chargers that have
// significantly different parameters, we will need to provide a way to
// dynamically configure these parameters, or else have the EC provide a PCHG0
// device directly to model the charger, and modify this logic to disable the
// synthetic charger when both a switch and a PCHG0 device are present.
// Millisecond period to update charge level when stylus is in garage
constexpr int kGarageChargeUpdatePeriod = 1000;
// Estimated maximum time to charge garaged stylus to 100%, in ms, plus margin
constexpr int kGaragedStylusChargeTime = 17 * 1000;
constexpr char kStylusGarageKey[] = "garaged-stylus-charger";
constexpr char16_t kStylusGarageName[] = u"Stylus Charger";
// Serial numbers for styluses which may report inconsistent battery levels.
const RE2 kBlockedStylusDevicesPattern(
"(?i)^(019|015|020|201|211|213)[0-9A-F]{5}(11|4[F0])FE368C$");
// Serial numbers for styluses which may report inconsistent battery levels,
// but might not actually exist in wild.
const RE2 kUnusualStylusDevicesPattern(
"(?i)^[0-9A-F]{3}[0-9A-F]{5}[0-9A-F]{2}FE368C$");
// Checks if the device is an external stylus.
bool IsStylusDevice(const std::string& path,
const std::string& model_name,
bool* has_garage) {
std::string identifier = ExtractHIDBatteryIdentifier(path);
for (const ui::TouchscreenDevice& device :
ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices()) {
if (device.has_stylus &&
(device.name == model_name ||
base::Contains(device.name, model_name)) &&
base::Contains(device.sys_path.value(), identifier)) {
*has_garage = device.has_stylus_garage_switch;
return true;
}
}
return false;
}
// Checks for devices which are ineligible for battery reports.
bool IsEligibleForBatteryReport(
PeripheralBatteryListener::BatteryInfo::PeripheralType type,
const std::string& serial_number) {
if (type != PeripheralBatteryListener::BatteryInfo::PeripheralType::
kStylusViaScreen &&
type != PeripheralBatteryListener::BatteryInfo::PeripheralType::
kStylusViaCharger)
return true;
// BUG(b/194132391): some firmwares do not report serial numbers,
// treat them as ineligible until this is resolved.
if (type == PeripheralBatteryListener::BatteryInfo::PeripheralType::
kStylusViaScreen &&
serial_number.empty()) {
base::UmaHistogramEnumeration(
kStylusBatteryReportingEligibilityHistogramName,
StylusBatteryReportingEligibility::kIneligibleDueToScreen);
return false;
}
if (serial_number.empty()) {
base::UmaHistogramEnumeration(
kStylusBatteryReportingEligibilityHistogramName,
StylusBatteryReportingEligibility::kEligible);
return true;
}
if (RE2::FullMatch(serial_number, kBlockedStylusDevicesPattern)) {
base::UmaHistogramEnumeration(
kStylusBatteryReportingEligibilityHistogramName,
StylusBatteryReportingEligibility::kIncorrectReports);
return false;
}
base::UmaHistogramEnumeration(kStylusBatteryReportingEligibilityHistogramName,
StylusBatteryReportingEligibility::kEligible);
// kUnusualStylusDevicesPattern and unrecognized devices are eligible
return true;
}
// Checks if device is the internal charger for an external stylus.
bool IsPeripheralCharger(const std::string& path) {
return RE2::PartialMatch(path, *kPeripheralChargerRegex);
}
std::string GetMapKeyForBluetoothAddress(const std::string& bluetooth_address) {
return kBluetoothDeviceIdPrefix + base::ToLowerASCII(bluetooth_address);
}
// Returns the corresponding map key for a HID device.
std::string GetBatteryMapKey(const std::string& path) {
// Check if the HID path corresponds to a Bluetooth device.
const std::string bluetooth_address =
ExtractBluetoothAddressFromHIDBatteryPath(path);
if (IsPeripheralCharger(path))
return kStylusChargerID;
else if (!bluetooth_address.empty())
return GetMapKeyForBluetoothAddress(bluetooth_address);
else
return path;
}
std::string GetBatteryMapKey(device::BluetoothDevice* device) {
return GetMapKeyForBluetoothAddress(device->GetAddress());
}
PeripheralBatteryListener::BatteryInfo::ChargeStatus
ConvertPowerManagerChargeStatus(
power_manager::PeripheralBatteryStatus_ChargeStatus incoming) {
switch (incoming) {
case power_manager::
PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_UNKNOWN:
return PeripheralBatteryListener::BatteryInfo::ChargeStatus::kUnknown;
case power_manager::
PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_DISCHARGING:
return PeripheralBatteryListener::BatteryInfo::ChargeStatus::kDischarging;
case power_manager::
PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_CHARGING:
return PeripheralBatteryListener::BatteryInfo::ChargeStatus::kCharging;
case power_manager::
PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_NOT_CHARGING:
return PeripheralBatteryListener::BatteryInfo::ChargeStatus::kNotCharging;
case power_manager::PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_FULL:
return PeripheralBatteryListener::BatteryInfo::ChargeStatus::kFull;
case power_manager::
PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_ERROR:
return PeripheralBatteryListener::BatteryInfo::ChargeStatus::kError;
}
}
} // namespace
PeripheralBatteryListener::BatteryInfo::BatteryInfo() = default;
PeripheralBatteryListener::BatteryInfo::BatteryInfo(
const std::string& key,
const std::u16string& name,
std::optional<uint8_t> level,
bool battery_report_eligible,
base::TimeTicks last_update_timestamp,
PeripheralType type,
ChargeStatus charge_status,
const std::string& bluetooth_address)
: key(key),
name(name),
level(level),
battery_report_eligible(battery_report_eligible),
last_update_timestamp(last_update_timestamp),
type(type),
charge_status(charge_status),
bluetooth_address(bluetooth_address) {}
PeripheralBatteryListener::BatteryInfo::~BatteryInfo() = default;
PeripheralBatteryListener::BatteryInfo::BatteryInfo(const BatteryInfo& info) =
default;
PeripheralBatteryListener::PeripheralBatteryListener() {
chromeos::PowerManagerClient::Get()->AddObserver(this);
ui::DeviceDataManager::GetInstance()->AddObserver(this);
device::BluetoothAdapterFactory::Get()->GetAdapter(
base::BindOnce(&PeripheralBatteryListener::InitializeOnBluetoothReady,
weak_factory_.GetWeakPtr()));
// When we are constructed, and device lists are ready, we want to get
// the backlog of any peripheral devices from the pmc; otherwise we only
// receive updates. If they aren't complete now, we'll catch it in the
// callback when they are.
if (ui::DeviceDataManager::GetInstance()->AreDeviceListsComplete())
chromeos::PowerManagerClient::Get()->RequestAllPeripheralBatteryUpdate();
}
PeripheralBatteryListener::~PeripheralBatteryListener() {
garage_charge_timer_.Stop();
if (bluetooth_adapter_)
bluetooth_adapter_->RemoveObserver(this);
ui::DeviceDataManager::GetInstance()->RemoveObserver(this);
if (chromeos::PowerManagerClient::Get())
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
}
bool PeripheralBatteryListener::HasSyntheticStylusGarargePeripheral() {
return synthetic_stylus_garage_peripheral_;
}
void PeripheralBatteryListener::UpdateSyntheticStylusGarargePeripheral() {
if (synthetic_stylus_garage_peripheral_)
return;
// When we start up, retrieve the current garage state. When we get
// it, if the stylus is in the garage, assume it has been there for a
// while and has a full charge. If not present, we cannot provide any
// information.
ui::OzonePlatform::GetInstance()->GetInputController()->GetStylusSwitchState(
base::BindOnce(&PeripheralBatteryListener::GetSwitchStateCallback,
weak_factory_.GetWeakPtr()));
synthetic_stylus_garage_peripheral_ = true;
}
void PeripheralBatteryListener::GetSwitchStateCallback(ui::StylusState state) {
BatteryInfo battery{
kStylusGarageKey,
kStylusGarageName,
(state == ui::StylusState::REMOVED) ? std::optional<uint8_t>(std::nullopt)
: 100,
/*battery_report_eligible=*/true,
base::TimeTicks::Now(),
BatteryInfo::PeripheralType::kStylusViaCharger,
(state == ui::StylusState::REMOVED) ? BatteryInfo::ChargeStatus::kUnknown
: BatteryInfo::ChargeStatus::kFull,
""};
UpdateBattery(battery, true);
}
// Observing chromeos::PowerManagerClient
void PeripheralBatteryListener::PeripheralBatteryStatusReceived(
const std::string& path,
const std::string& name,
int level,
power_manager::PeripheralBatteryStatus_ChargeStatus pmc_charge_status,
const std::string& serial_number,
bool active_update) {
// Note that zero levels are seen during boot on hid devices; a
// power_supply node may be created without a real charge level, and
// we must let it through to allow the BatteryInfo to be created as
// soon as we are aware of it.
if (level < -1 || level > 100) {
LOG(ERROR) << "Invalid battery level " << level << " for device " << name
<< " at path " << path;
return;
}
if (!IsHIDBattery(path) && !IsPeripheralCharger(path)) {
LOG(ERROR) << "Unsupported battery path " << path;
return;
}
if (!ui::DeviceDataManager::HasInstance() ||
!ui::DeviceDataManager::GetInstance()->AreDeviceListsComplete()) {
LOG(ERROR) << "Discarding peripheral battery notification before devices "
"are enumerated";
return;
}
BatteryInfo::PeripheralType type;
bool has_garage = false;
if (IsPeripheralCharger(path)) {
type = BatteryInfo::PeripheralType::kStylusViaCharger;
// TODO(b/187299765): Devices currently do not both real peripheral chargers
// and a stylus dock switch. Once that changes, this logic needs to be
// updated to ensure the synthetic peripheral is not created.
CHECK(!HasSyntheticStylusGarargePeripheral());
} else if (IsStylusDevice(path, name, &has_garage)) {
type = BatteryInfo::PeripheralType::kStylusViaScreen;
if (has_garage) {
UpdateSyntheticStylusGarargePeripheral();
}
} else {
type = BatteryInfo::PeripheralType::kOther;
}
std::string map_key = GetBatteryMapKey(path);
std::optional<uint8_t> opt_level;
// Discard: -1 level charge events, they, if they exist, are invalid.
// 0-level discharge events can come through when hid devices
// are created by the screen, and are not informative.
// 0-level charging events are possible for peripheral wireless charging,
// and are valid.
if (level == -1 ||
(level == 0 &&
(type == BatteryInfo::PeripheralType::kStylusViaScreen ||
type == BatteryInfo::PeripheralType::kStylusViaCharger) &&
pmc_charge_status !=
power_manager::
PeripheralBatteryStatus_ChargeStatus_CHARGE_STATUS_CHARGING)) {
opt_level = std::nullopt;
} else {
opt_level = level;
}
// TODO(kenalba): if ineligible should we keep opt_level as previously set,
// or clamp it to a fixed value?
bool battery_report_eligible =
IsEligibleForBatteryReport(type, serial_number);
PeripheralBatteryListener::BatteryInfo battery{
map_key,
base::ASCIIToUTF16(name),
opt_level,
battery_report_eligible,
base::TimeTicks::Now(),
type,
ConvertPowerManagerChargeStatus(pmc_charge_status),
ExtractBluetoothAddressFromHIDBatteryPath(path)};
UpdateBattery(battery, active_update);
}
// Observing device::BluetoothAdapter
void PeripheralBatteryListener::DeviceBatteryChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
device::BluetoothDevice::BatteryType type) {
// This class pre-dates the change to add multiple battery support. To reduce
// the risk of regressions, we're ignoring battery updates for any new battery
// types, and can revisit in the future if we decide there's a need for this
// class to add support.
if (type != device::BluetoothDevice::BatteryType::kDefault)
return;
std::optional<device::BluetoothDevice::BatteryInfo> info =
device->GetBatteryInfo(type);
if (info && info->percentage)
DCHECK_LE(info->percentage.value(), 100);
BatteryInfo battery{GetBatteryMapKey(device),
device->GetNameForDisplay(),
info.has_value() ? info->percentage : std::nullopt,
/*battery_report_eligible=*/true,
base::TimeTicks::Now(),
BatteryInfo::PeripheralType::kOther,
BatteryInfo::ChargeStatus::kUnknown,
device->GetAddress()};
// Bluetooth does not communicate charge state, do not fill in. Updates
// will generally pull from the remote device, so consider them active.
UpdateBattery(battery, /*active_update=*/true);
}
// Observing device::BluetoothAdapter
void PeripheralBatteryListener::DeviceConnectedStateChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
bool is_now_connected) {
if (!is_now_connected) {
RemoveBluetoothBattery(device->GetAddress());
return;
}
for (auto type : device->GetAvailableBatteryTypes()) {
std::optional<device::BluetoothDevice::BatteryInfo> info =
device->GetBatteryInfo(type);
DCHECK(info);
BatteryInfo::ChargeStatus charge_status;
switch (info->charge_state) {
case device::BluetoothDevice::BatteryInfo::ChargeState::kUnknown:
charge_status = BatteryInfo::ChargeStatus::kUnknown;
break;
case device::BluetoothDevice::BatteryInfo::ChargeState::kCharging:
charge_status = BatteryInfo::ChargeStatus::kCharging;
break;
case device::BluetoothDevice::BatteryInfo::ChargeState::kDischarging:
charge_status = BatteryInfo::ChargeStatus::kDischarging;
break;
}
BatteryInfo battery{GetBatteryMapKey(device),
device->GetNameForDisplay(),
info->percentage,
/*battery_report_eligible=*/true,
base::TimeTicks::Now(),
BatteryInfo::PeripheralType::kOther,
charge_status,
device->GetAddress()};
UpdateBattery(battery, /*active_update=*/true);
}
}
// Observing device::BluetoothAdapter
void PeripheralBatteryListener::DeviceRemoved(device::BluetoothAdapter* adapter,
device::BluetoothDevice* device) {
RemoveBluetoothBattery(device->GetAddress());
}
void PeripheralBatteryListener::InitializeOnBluetoothReady(
scoped_refptr<device::BluetoothAdapter> adapter) {
bluetooth_adapter_ = adapter;
CHECK(bluetooth_adapter_);
bluetooth_adapter_->AddObserver(this);
}
void PeripheralBatteryListener::RemoveBluetoothBattery(
const std::string& bluetooth_address) {
auto it = batteries_.find(kBluetoothDeviceIdPrefix +
base::ToLowerASCII(bluetooth_address));
if (it != batteries_.end()) {
NotifyRemovingBattery(it->second);
batteries_.erase(it);
}
}
// Observing ui::DeviceDataManager:
void PeripheralBatteryListener::OnDeviceListsComplete() {
chromeos::PowerManagerClient::Get()->RequestAllPeripheralBatteryUpdate();
}
// Present a charge level and charging/full state based on the prior value. We
// don't try to make an accurate estimate of charge level, as it could be
// completely wrong. We instead assume that charging will always take the
// maxmium amount of time, hold the charge level unchanged, at a max of 99% (not
// fully charged), until the maximum charge time expires. Then it is reported at
// 100% and full. This ensures it will not report full until it is _definitely_
// full, and we don't provide a worse estimate than we were already showing.
void PeripheralBatteryListener::GarageTimerAction(
base::TimeTicks charge_start_time,
std::optional<uint8_t> start_level) {
if (!synthetic_stylus_garage_peripheral_)
return;
auto it = batteries_.find(kStylusGarageKey);
if (it == batteries_.end()) {
return;
}
BatteryInfo info = it->second;
info.last_update_timestamp = base::TimeTicks::Now();
base::TimeDelta charge_period = base::TimeTicks::Now() - charge_start_time;
int new_level = start_level.has_value() ? *start_level : 1;
if (new_level < 1)
new_level = 1;
if (new_level >= 99)
new_level = 99;
// Consider it fully charged only after the max time has passed.
if (charge_period.InMilliseconds() >= kGaragedStylusChargeTime) {
info.level = 100;
info.charge_status = BatteryInfo::ChargeStatus::kFull;
garage_charge_timer_.Stop();
} else {
info.level = new_level;
info.charge_status = BatteryInfo::ChargeStatus::kCharging;
}
UpdateBattery(info, true);
}
std::optional<uint8_t> PeripheralBatteryListener::DerateLastChargeLevel() {
BatteryInfo latest_battery;
// Find the battery info with most recent data about the stylus
for (auto it : batteries_) {
if (it.second.type != BatteryInfo::PeripheralType::kStylusViaScreen &&
it.second.type != BatteryInfo::PeripheralType::kStylusViaCharger) {
continue;
}
if (!it.second.last_active_update_timestamp.has_value())
continue;
if (latest_battery.last_active_update_timestamp <
it.second.last_active_update_timestamp) {
latest_battery = it.second;
}
}
// No information available.
if (!latest_battery.level.has_value())
return std::nullopt;
int level = *latest_battery.level;
// We could do an estimate on charge level assuming a known discharge rate,
// however we cannot prove it is the same stylus, and the operation would be
// clearly incorrect if someone is swapping between two styluses to keep them
// charged. Instead we simply report the last known level, or 99 at max, just
// below full. This is not a correct estimate, but it is a useful value for
// the UX. (99 max means we will never immediately say the stylus is full, and
// using the last reading as minimum means we will never show 'low battery'
// unless it was already the case).
if (!level)
level = 1;
if (level >= 99)
level = 99;
return level;
}
void PeripheralBatteryListener::OnStylusStateChanged(
ui::StylusState stylus_state) {
if (!synthetic_stylus_garage_peripheral_)
return;
if (stylus_state == current_stylus_state_)
return;
auto it = batteries_.find(kStylusGarageKey);
if (it == batteries_.end())
return;
BatteryInfo info = it->second;
if (stylus_state == ui::StylusState::INSERTED) {
// Set charger level from last prior reading, minus the estimated discharge
// amount since the time of that last reading.
info.level = DerateLastChargeLevel();
info.charge_status = info.level >= 100
? BatteryInfo::ChargeStatus::kFull
: BatteryInfo::ChargeStatus::kCharging;
UpdateBattery(info, true);
if (info.charge_status == BatteryInfo::ChargeStatus::kCharging) {
base::TimeTicks charge_start_time = base::TimeTicks::Now();
garage_charge_timer_.Start(
FROM_HERE, base::Milliseconds(kGarageChargeUpdatePeriod),
base::BindRepeating(&PeripheralBatteryListener::GarageTimerAction,
base::Unretained(this), charge_start_time,
info.level));
} else {
garage_charge_timer_.Stop();
}
} else if (stylus_state == ui::StylusState::REMOVED) {
garage_charge_timer_.Stop();
// We leave the charge level unchanged, it may not be accurate, but
// it will be corrected once the stylus is used on the screen; any
// alternative (revising the estimate, or reverting to the value at
// the beginning of charge, if it wasn't fully charged) would lead to the
// level jumping when the stylus is removed from the garage.
info.charge_status = BatteryInfo::ChargeStatus::kUnknown;
UpdateBattery(info, true);
}
current_stylus_state_ = stylus_state;
}
void PeripheralBatteryListener::UpdateBattery(const BatteryInfo& battery_info,
bool active_update) {
const std::string& map_key = battery_info.key;
auto it = batteries_.find(map_key);
if (it == batteries_.end()) {
batteries_[map_key] = battery_info;
NotifyAddingBattery(batteries_[map_key]);
} else {
BatteryInfo& existing_battery_info = it->second;
// Only some fields should ever change.
DCHECK(existing_battery_info.bluetooth_address == battery_info.bluetooth_address);
DCHECK(existing_battery_info.type == battery_info.type);
existing_battery_info.name = battery_info.name;
// Ignore a null level for stylus charger updates: we want to memorize
// the last known actual value. (The touchscreen controller firmware
// already memorizes this, for that path).
if (battery_info.type != BatteryInfo::PeripheralType::kStylusViaCharger ||
battery_info.level)
existing_battery_info.level = battery_info.level;
existing_battery_info.last_update_timestamp =
battery_info.last_update_timestamp;
existing_battery_info.charge_status = battery_info.charge_status;
existing_battery_info.battery_report_eligible =
battery_info.battery_report_eligible;
}
BatteryInfo& info = batteries_[map_key];
if (active_update) {
info.last_active_update_timestamp = info.last_update_timestamp;
}
NotifyUpdatedBatteryLevel(info);
}
void PeripheralBatteryListener::NotifyAddingBattery(
const BatteryInfo& battery) {
for (auto& obs : observers_)
obs.OnAddingBattery(battery);
}
void PeripheralBatteryListener::NotifyRemovingBattery(
const BatteryInfo& battery) {
for (auto& obs : observers_)
obs.OnRemovingBattery(battery);
}
void PeripheralBatteryListener::NotifyUpdatedBatteryLevel(
const BatteryInfo& battery) {
for (auto& obs : observers_)
obs.OnUpdatedBatteryLevel(battery);
}
void PeripheralBatteryListener::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
// As possible latecomer, introduce observer to batteries that already exist.
for (auto it : batteries_) {
observer->OnAddingBattery(it.second);
observer->OnUpdatedBatteryLevel(it.second);
}
}
void PeripheralBatteryListener::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
bool PeripheralBatteryListener::HasObserver(const Observer* observer) const {
return observers_.HasObserver(observer);
}
} // namespace ash