chromium/ash/system/bluetooth/bluetooth_device_list_item_view.cc

// Copyright 2021 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/bluetooth/bluetooth_device_list_item_view.h"

#include <string>
#include <string_view>

#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/system/bluetooth/bluetooth_device_list_item_battery_view.h"
#include "ash/system/bluetooth/bluetooth_device_list_item_multiple_battery_view.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_utils.h"
#include "base/check.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "chromeos/ash/services/bluetooth_config/public/cpp/cros_bluetooth_config_util.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "mojo/public/cpp/bindings/clone_traits.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"

namespace ash {

namespace {

using bluetooth_config::GetPairedDeviceName;
using bluetooth_config::mojom::BatteryPropertiesPtr;
using bluetooth_config::mojom::DeviceBatteryInfoPtr;
using bluetooth_config::mojom::DeviceConnectionState;
using bluetooth_config::mojom::DeviceType;
using bluetooth_config::mojom::PairedBluetoothDevicePropertiesPtr;

constexpr int kEnterpriseManagedIconSizeDip = 20;

bool HasMultipleBatteryInfos(const DeviceBatteryInfoPtr& battery_info) {
  DCHECK(battery_info);
  return battery_info->left_bud_info || battery_info->case_info ||
         battery_info->right_bud_info;
}

// Returns the text ID corresponding to the provided |device_connection_state|.
int GetDeviceConnectionStateA11yTextId(
    const DeviceConnectionState device_connection_state) {
  switch (device_connection_state) {
    case DeviceConnectionState::kConnected:
      return IDS_BLUETOOTH_A11Y_DEVICE_CONNECTION_STATE_CONNECTED;
    case DeviceConnectionState::kConnecting:
      return IDS_BLUETOOTH_A11Y_DEVICE_CONNECTION_STATE_CONNECTING;
    case DeviceConnectionState::kNotConnected:
      return IDS_BLUETOOTH_A11Y_DEVICE_CONNECTION_STATE_NOT_CONNECTED;
  }
  NOTREACHED();
}

// Returns the text ID corresponding to the provided |device_type|.
int GetDeviceTypeA11yTextId(const DeviceType device_type) {
  switch (device_type) {
    case DeviceType::kComputer:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_COMPUTER;
    case DeviceType::kPhone:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_PHONE;
    case DeviceType::kHeadset:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_HEADSET;
    case DeviceType::kVideoCamera:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_VIDEO_CAMERA;
    case DeviceType::kGameController:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_GAME_CONTROLLER;
    case DeviceType::kKeyboard:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_KEYBOARD;
    case DeviceType::kKeyboardMouseCombo:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_KEYBOARD_MOUSE_COMBO;
    case DeviceType::kMouse:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_MOUSE;
    case DeviceType::kTablet:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_TABLET;
    case DeviceType::kUnknown:
      return IDS_BLUETOOTH_A11Y_DEVICE_TYPE_UNKNOWN;
  }
  NOTREACHED();
}

// Returns the formatted a11y text describing the battery information of the
// provided |battery_info|.
const std::u16string GetDeviceBatteryA11yText(
    const DeviceBatteryInfoPtr& battery_info) {
  if (!battery_info) {
    return std::u16string();
  }

  if (HasMultipleBatteryInfos(battery_info)) {
    std::u16string battery_text;

    auto add_battery_text_if_exists =
        [&battery_text](const BatteryPropertiesPtr& battery_properties,
                        int text_id) {
          if (!battery_properties) {
            return;
          }
          if (!battery_text.empty()) {
            battery_text = base::StrCat({battery_text, u" "});
          }
          battery_text = base::StrCat(
              {battery_text,
               l10n_util::GetStringFUTF16(
                   text_id, base::NumberToString16(
                                battery_properties->battery_percentage))});
        };

    add_battery_text_if_exists(
        battery_info->left_bud_info,
        IDS_BLUETOOTH_A11Y_DEVICE_NAMED_BATTERY_INFO_LEFT_BUD);
    add_battery_text_if_exists(
        battery_info->case_info,
        IDS_BLUETOOTH_A11Y_DEVICE_NAMED_BATTERY_INFO_CASE);
    add_battery_text_if_exists(
        battery_info->right_bud_info,
        IDS_BLUETOOTH_A11Y_DEVICE_NAMED_BATTERY_INFO_RIGHT_BUD);

    return battery_text;
  }

  if (battery_info->default_properties) {
    return l10n_util::GetStringFUTF16(
        IDS_BLUETOOTH_A11Y_DEVICE_BATTERY_INFO,
        base::NumberToString16(
            battery_info->default_properties->battery_percentage));
  }
  return std::u16string();
}

// Returns the icon corresponding to the provided |device_type| and
// |connection_state|.
const gfx::VectorIcon& GetDeviceIcon(const DeviceType device_type) {
  switch (device_type) {
    case DeviceType::kComputer:
      return ash::kSystemMenuComputerIcon;
    case DeviceType::kPhone:
      return ash::kSystemMenuPhoneIcon;
    case DeviceType::kHeadset:
      return ash::kSystemMenuHeadsetIcon;
    case DeviceType::kVideoCamera:
      return ash::kSystemMenuVideocamIcon;
    case DeviceType::kGameController:
      return ash::kSystemMenuGamepadIcon;
    case DeviceType::kKeyboard:
      return ash::kSystemMenuKeyboardIcon;
    case DeviceType::kKeyboardMouseCombo:
      return ash::kSystemMenuKeyboardIcon;
    case DeviceType::kMouse:
      return ash::kSystemMenuMouseIcon;
    case DeviceType::kTablet:
      return ash::kSystemMenuTabletIcon;
    case DeviceType::kUnknown:
      return ash::kSystemMenuBluetoothIcon;
  }
  NOTREACHED();
}

}  // namespace

BluetoothDeviceListItemView::BluetoothDeviceListItemView(
    ViewClickListener* listener)
    : HoverHighlightView(listener) {}

BluetoothDeviceListItemView::~BluetoothDeviceListItemView() = default;

void BluetoothDeviceListItemView::UpdateDeviceProperties(
    size_t device_index,
    size_t total_device_count,
    const PairedBluetoothDevicePropertiesPtr& device_properties) {
  device_properties_ = mojo::Clone(device_properties);

  // We can only add an icon and label if the view has not already been
  // populated with one or both of these views. For simplicity, instead of
  // trying to determine which views exist and modifying them, and creating the
  // missing views, we instead clear all of the views and recreate them.
  if (is_populated()) {
    Reset();
  }

  const DeviceType& device_type =
      device_properties_->device_properties->device_type;

  AddIconAndLabel(ui::ImageModel::FromVectorIcon(
                      GetDeviceIcon(device_type),
                      static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface)),
                  GetPairedDeviceName(device_properties_));
  text_label()->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(ash::TypographyToken::kCrosButton2,
                                        *text_label());

  UpdateAccessibleName(device_index, total_device_count);

  // Adds an icon to indicate that the device supports profiles or services that
  // are disabled or blocked by enterprise policy.
  if (device_properties->device_properties->is_blocked_by_policy) {
    AddRightIcon(ui::ImageModel::FromVectorIcon(
                     chromeos::kEnterpriseIcon,
                     static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface),
                     kEnterpriseManagedIconSizeDip),
                 /*icon_size=*/kEnterpriseManagedIconSizeDip);
  }

  const DeviceConnectionState& connection_state =
      device_properties_->device_properties->connection_state;

  // Adds a sub-label to show that the device is in the process of connecting.
  if (connection_state == DeviceConnectionState::kConnecting) {
    SetupConnectingScrollListItem(this);
  } else if (connection_state == DeviceConnectionState::kConnected) {
    UpdateBatteryInfo(device_properties_->device_properties->battery_info);
  }
}

void BluetoothDeviceListItemView::UpdateAccessibleName(
    size_t device_index,
    size_t total_device_count) {
  DCHECK(device_properties_);

  // It is not best practice to concatenate translated strings together, but in
  // this case we would have an explosion in the number of strings if we had a
  // unique string for each permutation. Instead, below we concatenate related
  // but complete sentences here with a hard stop, e.g. a period.

  // Add the device name information.
  std::u16string a11y_text = l10n_util::GetStringFUTF16(
      IDS_BLUETOOTH_A11Y_DEVICE_NAME, base::NumberToString16(device_index + 1),
      base::NumberToString16(total_device_count),
      GetPairedDeviceName(device_properties_));

  // Add the device connection status information.
  a11y_text = base::StrCat(
      {a11y_text, u" ",
       l10n_util::GetStringUTF16(GetDeviceConnectionStateA11yTextId(
           device_properties_->device_properties->connection_state))});

  // Add the device type information.
  a11y_text =
      base::StrCat({a11y_text, u" ",
                    l10n_util::GetStringUTF16(GetDeviceTypeA11yTextId(
                        device_properties_->device_properties->device_type))});

  const std::u16string battery_text = GetDeviceBatteryA11yText(
      device_properties_->device_properties->battery_info);

  if (!battery_text.empty()) {
    a11y_text = base::StrCat({a11y_text, u" ", battery_text});
  }

  GetViewAccessibility().SetName(a11y_text);
}

void BluetoothDeviceListItemView::UpdateBatteryInfo(
    const DeviceBatteryInfoPtr& battery_info) {
  if (!battery_info || (!battery_info->default_properties &&
                        !HasMultipleBatteryInfos(battery_info))) {
    sub_row()->RemoveAllChildViews();
    return;
  }

  if (HasMultipleBatteryInfos(battery_info)) {
    UpdateMultipleBatteryView(battery_info);
    return;
  }

  UpdateSingleBatteryView(battery_info);
}

void BluetoothDeviceListItemView::UpdateMultipleBatteryView(
    const DeviceBatteryInfoPtr& battery_info) {
  // Remove battery view if it is not a multiple battery view.
  if (!sub_row()->children().empty()) {
    DCHECK(sub_row()->children().size() == 1);
    if (std::string_view(sub_row()->children().at(0)->GetClassName()) !=
        std::string_view(
            BluetoothDeviceListItemMultipleBatteryView::kViewClassName)) {
      sub_row()->RemoveAllChildViews();
    }
  }

  BluetoothDeviceListItemMultipleBatteryView* battery_view = nullptr;

  // Add multiple battery view if missing.
  if (sub_row()->children().empty()) {
    battery_view = sub_row()->AddChildView(
        std::make_unique<BluetoothDeviceListItemMultipleBatteryView>());
  } else {
    DCHECK_EQ(1u, sub_row()->children().size());
    battery_view = static_cast<BluetoothDeviceListItemMultipleBatteryView*>(
        sub_row()->children().at(0));
  }

  // Update multiple battery view.
  battery_view->UpdateBatteryInfo(battery_info);
}

void BluetoothDeviceListItemView::UpdateSingleBatteryView(
    const DeviceBatteryInfoPtr& battery_info) {
  // Remove battery view if it is not a single battery view.
  if (!sub_row()->children().empty()) {
    DCHECK(sub_row()->children().size() == 1);
    if (std::string_view(sub_row()->children().at(0)->GetClassName()) !=
        std::string_view(BluetoothDeviceListItemBatteryView::kViewClassName)) {
      sub_row()->RemoveAllChildViews();
    }
  }

  BluetoothDeviceListItemBatteryView* battery_view = nullptr;

  // Add single battery view if missing.
  if (sub_row()->children().empty()) {
    battery_view = sub_row()->AddChildView(
        std::make_unique<BluetoothDeviceListItemBatteryView>());
  } else {
    DCHECK_EQ(1u, sub_row()->children().size());
    battery_view = static_cast<BluetoothDeviceListItemBatteryView*>(
        sub_row()->children().at(0));
  }

  // Update single battery view.
  battery_view->UpdateBatteryInfo(
      battery_info->default_properties->battery_percentage,
      IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_BATTERY_PERCENTAGE_ONLY_LABEL);
}

BEGIN_METADATA(BluetoothDeviceListItemView)
END_METADATA

}  // namespace ash