chromium/ash/system/power/peripheral_battery_notifier.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/power/peripheral_battery_notifier.h"

#include <string>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/power/hid_battery_util.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "components/device_event_log/device_event_log.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.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/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/message_center/public/cpp/notification_delegate.h"

namespace ash {

namespace {

// When a peripheral device's battery level is <= kLowBatteryLevel, consider
// it to be in low battery condition.
const uint8_t kLowBatteryLevel = 16;

// Don't show 2 low battery notification within |kNotificationInterval|.
constexpr base::TimeDelta kNotificationInterval = base::Seconds(60);

constexpr char kNotifierStylusBattery[] = "ash.stylus-battery";

constexpr char kNotificationOriginUrl[] = "chrome://peripheral-battery";
constexpr char kNotifierNonStylusBattery[] = "power.peripheral-battery";

// Prefix added to the key of a device to generate a unique ID when posting
// a notification to the Message Center.
constexpr char kPeripheralDeviceIdPrefix[] = "battery_notification-";

// Struct containing parameters for the notification which vary between the
// stylus notifications and the non stylus notifications.
struct NotificationParams {
  std::string id;
  std::u16string title;
  std::u16string message;
  std::string notifier_name;
  GURL url;
  raw_ptr<const gfx::VectorIcon> icon;
};

NotificationParams GetNonStylusNotificationParams(const std::string& map_key,
                                                  const std::u16string& name,
                                                  uint8_t battery_level,
                                                  bool is_bluetooth) {
  return NotificationParams{
      kPeripheralDeviceIdPrefix + map_key,
      name,
      l10n_util::GetStringFUTF16Int(
          IDS_ASH_LOW_PERIPHERAL_BATTERY_NOTIFICATION_TEXT, battery_level),
      kNotifierNonStylusBattery,
      GURL(kNotificationOriginUrl),
      is_bluetooth ? &kNotificationBluetoothBatteryWarningIcon
                   : &kNotificationBatteryCriticalIcon};
}

NotificationParams GetStylusNotificationParams() {
  return NotificationParams{
      PeripheralBatteryNotifier::kStylusNotificationId,
      l10n_util::GetStringUTF16(IDS_ASH_LOW_STYLUS_BATTERY_NOTIFICATION_TITLE),
      l10n_util::GetStringUTF16(IDS_ASH_LOW_STYLUS_BATTERY_NOTIFICATION_BODY),
      kNotifierStylusBattery,
      GURL(),
      &kNotificationStylusBatteryWarningIcon};
}

}  // namespace

const char PeripheralBatteryNotifier::kStylusNotificationId[] =
    "stylus-battery";

PeripheralBatteryNotifier::NotificationInfo::NotificationInfo() = default;

PeripheralBatteryNotifier::NotificationInfo::NotificationInfo(
    std::optional<uint8_t> level,
    base::TimeTicks last_notification_timestamp)
    : level(level),
      last_notification_timestamp(last_notification_timestamp),
      ever_notified(false) {}

PeripheralBatteryNotifier::NotificationInfo::~NotificationInfo() = default;

PeripheralBatteryNotifier::NotificationInfo::NotificationInfo(
    const NotificationInfo& info) {
  level = info.level;
  last_notification_timestamp = info.last_notification_timestamp;
  ever_notified = info.ever_notified;
}

PeripheralBatteryNotifier::PeripheralBatteryNotifier(
    PeripheralBatteryListener* listener)
    : peripheral_battery_listener_(listener) {
  peripheral_battery_listener_->AddObserver(this);
}

PeripheralBatteryNotifier::~PeripheralBatteryNotifier() {
  peripheral_battery_listener_->RemoveObserver(this);
}

void PeripheralBatteryNotifier::OnUpdatedBatteryLevel(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  // TODO(b/187703348): it is worth listening to charger events if they
  // might remove the notification: we want to clear it as soon as
  // we believe the battery to have been charged, or at least starting
  // charging.
  if (battery_info.type == PeripheralBatteryListener::BatteryInfo::
                               PeripheralType::kStylusViaCharger) {
    return;
  }
  UpdateBattery(battery_info);
}

void PeripheralBatteryNotifier::OnAddingBattery(
    const PeripheralBatteryListener::BatteryInfo& battery) {}

void PeripheralBatteryNotifier::OnRemovingBattery(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  const std::string& map_key = battery_info.key;
  CancelNotification(battery_info);
  battery_notifications_.erase(map_key);
  BLUETOOTH_LOG(EVENT)
      << "Battery has been removed, erasing notification, battery key: "
      << map_key;
}

void PeripheralBatteryNotifier::UpdateBattery(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  if (!battery_info.level || !battery_info.battery_report_eligible) {
    CancelNotification(battery_info);
    return;
  }

  const std::string& map_key = battery_info.key;
  bool was_old_battery_level_low = false;
  auto it = battery_notifications_.find(map_key);

  if (it == battery_notifications_.end()) {
    NotificationInfo new_notification_info;
    new_notification_info.level = battery_info.level;
    new_notification_info.last_notification_timestamp =
        battery_info.last_update_timestamp;
    new_notification_info.ever_notified = false;
    battery_notifications_[map_key] = new_notification_info;
  } else {
    NotificationInfo& existing_notification_info = it->second;
    std::optional<uint8_t> old_level = existing_notification_info.level;
    was_old_battery_level_low = old_level && *old_level <= kLowBatteryLevel;
    existing_notification_info.level = battery_info.level;

    BLUETOOTH_LOG(EVENT)
        << "Battery level updated, battery key: " << map_key
        << " new battery level: "
        << (battery_info.level.has_value()
                ? base::NumberToString(battery_info.level.value())
                : "empty value")
        << " old battery level: "
        << (old_level.has_value() ? base::NumberToString(old_level.value())
                                  : "empty value")
        << " battery type: " << static_cast<int>(battery_info.type);
  }

  if (*battery_info.level > kLowBatteryLevel) {
    CancelNotification(battery_info);
    return;
  }

  // If low battery was on record, check if there is a notification, otherwise
  // the user dismissed it and we shouldn't create another one.
  if (was_old_battery_level_low)
    UpdateBatteryNotificationIfVisible(battery_info);
  else
    ShowNotification(battery_info);
}

void PeripheralBatteryNotifier::UpdateBatteryNotificationIfVisible(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  const std::string& map_key = battery_info.key;
  std::string notification_map_key =
      (battery_info.type ==
       PeripheralBatteryListener::BatteryInfo::PeripheralType::kStylusViaScreen)
          ? kStylusNotificationId
          : (kPeripheralDeviceIdPrefix + map_key);
  message_center::Notification* notification =
      message_center::MessageCenter::Get()->FindVisibleNotificationById(
          notification_map_key);
  if (notification)
    ShowOrUpdateNotification(battery_info);
}

void PeripheralBatteryNotifier::ShowNotification(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  const std::string& map_key = battery_info.key;
  NotificationInfo& notification_info = battery_notifications_[map_key];
  base::TimeTicks now = base::TimeTicks::Now();
  if (!notification_info.ever_notified ||
      (now - notification_info.last_notification_timestamp >=
       kNotificationInterval)) {
    ShowOrUpdateNotification(battery_info);
    notification_info.last_notification_timestamp = base::TimeTicks::Now();
    notification_info.ever_notified = true;
  }
}

void PeripheralBatteryNotifier::ShowOrUpdateNotification(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  const std::string& map_key = battery_info.key;

  BLUETOOTH_LOG(EVENT)
      << "Battery notification shown or updated, battery key: " << map_key
      << " battery level: "
      << (battery_info.level.has_value()
              ? base::NumberToString(battery_info.level.value())
              : "empty value")
      << " battery type: " << static_cast<int>(battery_info.type);

  // Stylus battery notifications differ slightly.
  NotificationParams params =
      (battery_info.type ==
       PeripheralBatteryListener::BatteryInfo::PeripheralType::kStylusViaScreen)
          ? GetStylusNotificationParams()
          : GetNonStylusNotificationParams(
                map_key, battery_info.name, *battery_info.level,
                !battery_info.bluetooth_address.empty());

  auto delegate = base::MakeRefCounted<
      message_center::HandleNotificationClickDelegate>(base::BindRepeating(
      [](const NotificationParams& params) {
        Shell::Get()->system_tray_model()->client()->ShowBluetoothSettings();
        message_center::MessageCenter::Get()->RemoveNotification(
            params.id, /*by_user=*/false);
      },
      params));

  auto notification = CreateSystemNotificationPtr(
      message_center::NOTIFICATION_TYPE_SIMPLE, params.id, params.title,
      params.message, std::u16string(), params.url,
      message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
                                 params.notifier_name,
                                 NotificationCatalogName::kPeripheralBattery),
      message_center::RichNotificationData(), std::move(delegate), *params.icon,
      message_center::SystemNotificationWarningLevel::WARNING);

  message_center::MessageCenter::Get()->AddNotification(
      std::move(notification));
}

void PeripheralBatteryNotifier::CancelNotification(
    const PeripheralBatteryListener::BatteryInfo& battery_info) {
  const std::string& map_key = battery_info.key;
  const auto it = battery_notifications_.find(map_key);

  if (it == battery_notifications_.end()) {
    return;
  }

  BLUETOOTH_LOG(EVENT)
      << "Battery notification canceled, battery key: " << map_key
      << " battery level: "
      << (battery_info.level.has_value()
              ? base::NumberToString(battery_info.level.value())
              : "empty value")
      << " battery type: " << static_cast<int>(battery_info.type);

  std::string notification_map_key =
      (battery_info.type ==
       PeripheralBatteryListener::BatteryInfo::PeripheralType::kStylusViaScreen)
          ? kStylusNotificationId
          : (kPeripheralDeviceIdPrefix + map_key);

  message_center::MessageCenter::Get()->RemoveNotification(notification_map_key,
                                                           /*by_user=*/false);

  // Resetting this value allows a new low battery level to post a
  // notification if the old one was also under the threshold.
  it->second.level.reset();
}

}  // namespace ash