[go: nahoru, domu]

blob: a0ffcd3a219a32bdaae78a4edb17c86b71d860db [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_detailed_view.h"
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#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/style/ash_color_provider.h"
#include "ash/system/machine_learning/user_settings_event_logger.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_info_label.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "services/device/public/cpp/bluetooth/bluetooth_utils.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout.h"
using device::mojom::BluetoothDeviceInfo;
using device::mojom::BluetoothSystem;
namespace ash {
namespace tray {
namespace {
const int kDisabledPanelLabelBaselineY = 20;
// Returns corresponding device type icons for given Bluetooth device types and
// connection states.
const gfx::VectorIcon& GetBluetoothDeviceIcon(
BluetoothDeviceInfo::DeviceType device_type,
BluetoothDeviceInfo::ConnectionState connection_state) {
switch (device_type) {
case BluetoothDeviceInfo::DeviceType::kComputer:
return ash::kSystemMenuComputerIcon;
case BluetoothDeviceInfo::DeviceType::kPhone:
return ash::kSystemMenuPhoneIcon;
case BluetoothDeviceInfo::DeviceType::kAudio:
case BluetoothDeviceInfo::DeviceType::kCarAudio:
return ash::kSystemMenuHeadsetIcon;
case BluetoothDeviceInfo::DeviceType::kVideo:
return ash::kSystemMenuVideocamIcon;
case BluetoothDeviceInfo::DeviceType::kJoystick:
case BluetoothDeviceInfo::DeviceType::kGamepad:
return ash::kSystemMenuGamepadIcon;
case BluetoothDeviceInfo::DeviceType::kKeyboard:
case BluetoothDeviceInfo::DeviceType::kKeyboardMouseCombo:
return ash::kSystemMenuKeyboardIcon;
case BluetoothDeviceInfo::DeviceType::kTablet:
return ash::kSystemMenuTabletIcon;
case BluetoothDeviceInfo::DeviceType::kMouse:
return ash::kSystemMenuMouseIcon;
case BluetoothDeviceInfo::DeviceType::kModem:
case BluetoothDeviceInfo::DeviceType::kPeripheral:
return ash::kSystemMenuBluetoothIcon;
default:
return connection_state ==
BluetoothDeviceInfo::ConnectionState::kConnected
? ash::kSystemMenuBluetoothConnectedIcon
: ash::kSystemMenuBluetoothIcon;
}
}
views::View* CreateDisabledPanel() {
views::View* container = new views::View;
auto box_layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical);
box_layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kCenter);
container->SetLayoutManager(std::move(box_layout));
auto* color_provider = AshColorProvider::Get();
auto* image_view =
container->AddChildView(std::make_unique<views::ImageView>());
image_view->SetImage(gfx::CreateVectorIcon(
kSystemMenuBluetoothDisabledIcon,
AshColorProvider::GetDisabledColor(color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary))));
image_view->SetVerticalAlignment(views::ImageView::Alignment::kTrailing);
auto* label = container->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISABLED)));
label->SetEnabledColor(
AshColorProvider::GetDisabledColor(color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorPrimary)));
TrayPopupUtils::SetLabelFontList(
label, TrayPopupUtils::FontStyle::kDetailedViewLabel);
label->SetBorder(views::CreateEmptyBorder(
kDisabledPanelLabelBaselineY - label->GetBaseline(), 0, 0, 0));
// Make top padding of the icon equal to the height of the label so that the
// icon is vertically aligned to center of the container.
image_view->SetBorder(
views::CreateEmptyBorder(label->GetPreferredSize().height(), 0, 0, 0));
return container;
}
void LogUserBluetoothEvent(const BluetoothAddress& device_address) {
ml::UserSettingsEventLogger* logger = ml::UserSettingsEventLogger::Get();
if (logger) {
logger->LogBluetoothUkmEvent(device_address);
}
}
HoverHighlightView* GetScrollListItemForDevice(
const std::unordered_map<HoverHighlightView*, BluetoothAddress>& device_map,
const BluetoothAddress& address) {
for (const auto& view_and_address : device_map) {
if (view_and_address.second == address)
return view_and_address.first;
}
return nullptr;
}
} // namespace
BluetoothDetailedView::BluetoothDetailedView(DetailedViewDelegate* delegate,
LoginStatus login)
: TrayDetailedView(delegate), login_(login) {
CreateItems();
}
BluetoothDetailedView::~BluetoothDetailedView() = default;
void BluetoothDetailedView::ShowLoadingIndicator() {
// Setting a value of -1 gives progress_bar an infinite-loading behavior.
ShowProgress(-1, true);
}
void BluetoothDetailedView::HideLoadingIndicator() {
ShowProgress(0, false);
}
void BluetoothDetailedView::ShowBluetoothDisabledPanel() {
device_map_.clear();
paired_devices_heading_ = nullptr;
unpaired_devices_heading_ = nullptr;
bluetooth_discovering_label_ = nullptr;
scroll_content()->RemoveAllChildViews(true);
DCHECK(scroller());
if (!disabled_panel_) {
disabled_panel_ = CreateDisabledPanel();
// Insert |disabled_panel_| before the scroller, since the scroller will
// have unnecessary bottom border when it is not the last child.
AddChildViewAt(disabled_panel_, GetIndexOf(scroller()));
// |disabled_panel_| need to fill the remaining space below the title row
// so that the inner contents of |disabled_panel_| are placed properly.
box_layout()->SetFlexForView(disabled_panel_, 1);
}
disabled_panel_->SetVisible(true);
scroller()->SetVisible(false);
Layout();
}
void BluetoothDetailedView::HideBluetoothDisabledPanel() {
DCHECK(scroller());
if (disabled_panel_)
disabled_panel_->SetVisible(false);
scroller()->SetVisible(true);
Layout();
}
bool BluetoothDetailedView::IsDeviceScrollListEmpty() const {
return device_map_.empty();
}
void BluetoothDetailedView::UpdateDeviceScrollList(
const BluetoothDeviceList& connected_devices,
const BluetoothDeviceList& connecting_devices,
const BluetoothDeviceList& paired_not_connected_devices,
const BluetoothDeviceList& discovered_not_paired_devices) {
connecting_devices_.clear();
for (const auto& device : connecting_devices)
connecting_devices_.push_back(device->Clone());
paired_not_connected_devices_.clear();
for (const auto& device : paired_not_connected_devices)
paired_not_connected_devices_.push_back(device->Clone());
// Keep track of previous device_map_ so that existing scroll list
// item views can be re-used. This is required for a11y so that
// keyboard focus and screen-reader call outs are not disrupted
// by frequent device list updates.
std::unordered_map<HoverHighlightView*, BluetoothAddress> old_device_map =
device_map_;
device_map_.clear();
// Add paired devices and their section header to the list.
bool has_paired_devices = !connected_devices.empty() ||
!connecting_devices.empty() ||
!paired_not_connected_devices.empty();
int index = 0;
if (has_paired_devices) {
paired_devices_heading_ =
AddSubHeading(IDS_ASH_STATUS_TRAY_BLUETOOTH_PAIRED_DEVICES,
paired_devices_heading_, index++);
index = AddSameTypeDevicesToScrollList(connected_devices, old_device_map,
index, true, true);
index = AddSameTypeDevicesToScrollList(connecting_devices, old_device_map,
index, true, false);
index = AddSameTypeDevicesToScrollList(paired_not_connected_devices,
old_device_map, index, false, false);
} else if (paired_devices_heading_) {
scroll_content()->RemoveChildView(paired_devices_heading_);
paired_devices_heading_ = nullptr;
}
// Add unpaired devices to the list. If at least one paired device is
// present, also add a section header above the unpaired devices.
if (!discovered_not_paired_devices.empty()) {
if (has_paired_devices) {
unpaired_devices_heading_ =
AddSubHeading(IDS_ASH_STATUS_TRAY_BLUETOOTH_UNPAIRED_DEVICES,
unpaired_devices_heading_, index++);
}
index = AddSameTypeDevicesToScrollList(discovered_not_paired_devices,
old_device_map, index, false, false);
}
if (unpaired_devices_heading_ &&
(discovered_not_paired_devices.empty() || !has_paired_devices)) {
scroll_content()->RemoveChildView(unpaired_devices_heading_);
unpaired_devices_heading_ = nullptr;
}
// Show user Bluetooth state if there is no bluetooth devices in list.
if (device_map_.empty()) {
if (!bluetooth_discovering_label_) {
bluetooth_discovering_label_ =
new TrayInfoLabel(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERING);
scroll_content()->AddChildViewAt(bluetooth_discovering_label_, index++);
} else {
scroll_content()->ReorderChildView(bluetooth_discovering_label_, index++);
}
} else if (bluetooth_discovering_label_) {
scroll_content()->RemoveChildView(bluetooth_discovering_label_);
bluetooth_discovering_label_ = nullptr;
}
// Remove views for devices from old_device_map that are not in device_map_.
for (auto& view_and_address : old_device_map) {
if (device_map_.find(view_and_address.first) == device_map_.end()) {
scroll_content()->RemoveChildView(view_and_address.first);
}
}
scroll_content()->InvalidateLayout();
Layout();
}
void BluetoothDetailedView::SetToggleIsOn(bool is_on) {
if (toggle_)
toggle_->AnimateIsOn(is_on);
}
const char* BluetoothDetailedView::GetClassName() const {
return "BluetoothDetailedView";
}
void BluetoothDetailedView::CreateItems() {
CreateScrollableList();
scroll_content()->SetID(kScrollContentID);
CreateTitleRow(IDS_ASH_STATUS_TRAY_BLUETOOTH);
}
TriView* BluetoothDetailedView::AddSubHeading(int text_id,
TriView* sub_heading_view,
int child_index) {
if (!sub_heading_view) {
sub_heading_view = AddScrollListSubHeader(text_id);
}
scroll_content()->ReorderChildView(sub_heading_view, child_index);
return sub_heading_view;
}
int BluetoothDetailedView::AddSameTypeDevicesToScrollList(
const BluetoothDeviceList& list,
const std::unordered_map<HoverHighlightView*, BluetoothAddress>&
old_device_list,
int child_index,
bool highlight,
bool checked) {
for (const auto& device : list) {
const gfx::VectorIcon& icon =
GetBluetoothDeviceIcon(device->device_type, device->connection_state);
std::u16string device_name =
device::GetBluetoothDeviceNameForDisplay(device);
HoverHighlightView* container =
GetScrollListItemForDevice(old_device_list, device->address);
if (!container) {
container = AddScrollListItem(icon, device_name);
} else {
container->text_label()->SetText(device_name);
container->left_icon()->SetImage(gfx::CreateVectorIcon(
icon, AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary)));
}
container->SetAccessibleName(
device::GetBluetoothDeviceLabelForAccessibility(device));
switch (device->connection_state) {
case BluetoothDeviceInfo::ConnectionState::kNotConnected:
break;
case BluetoothDeviceInfo::ConnectionState::kConnecting:
SetupConnectingScrollListItem(container);
break;
case BluetoothDeviceInfo::ConnectionState::kConnected:
SetupConnectedScrollListItem(
container, device->battery_info
? base::make_optional<uint8_t>(
device->battery_info->battery_percentage)
: base::nullopt);
break;
}
scroll_content()->ReorderChildView(container, child_index++);
device_map_[container] = device->address;
}
return child_index;
}
bool BluetoothDetailedView::FoundDevice(
const BluetoothAddress& device_address,
const BluetoothDeviceList& device_list) const {
for (const auto& device : device_list) {
if (device->address == device_address)
return true;
}
return false;
}
void BluetoothDetailedView::UpdateClickedDevice(
const BluetoothAddress& device_address,
HoverHighlightView* item_container) {
if (FoundDevice(device_address, paired_not_connected_devices_)) {
SetupConnectingScrollListItem(item_container);
scroll_content()->SizeToPreferredSize();
scroller()->Layout();
}
}
void BluetoothDetailedView::ToggleButtonPressed() {
Shell::Get()->tray_bluetooth_helper()->SetBluetoothEnabled(
toggle_->GetIsOn());
}
void BluetoothDetailedView::ShowSettings() {
if (TrayPopupUtils::CanOpenWebUISettings()) {
CloseBubble(); // Deletes |this|.
Shell::Get()->system_tray_model()->client()->ShowBluetoothSettings();
}
}
base::Optional<BluetoothAddress>
BluetoothDetailedView::GetFocusedDeviceAddress() const {
for (const auto& view_and_address : device_map_) {
if (view_and_address.first->HasFocus())
return view_and_address.second;
}
return base::nullopt;
}
void BluetoothDetailedView::FocusDeviceByAddress(
const BluetoothAddress& address) const {
for (auto& view_and_address : device_map_) {
if (view_and_address.second == address) {
view_and_address.first->RequestFocus();
return;
}
}
}
void BluetoothDetailedView::HandleViewClicked(views::View* view) {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
if (helper->GetBluetoothState() != BluetoothSystem::State::kPoweredOn)
return;
HoverHighlightView* container = static_cast<HoverHighlightView*>(view);
std::unordered_map<HoverHighlightView*, BluetoothAddress>::iterator find;
find = device_map_.find(container);
if (find == device_map_.end())
return;
const BluetoothAddress& device_address = find->second;
if (FoundDevice(device_address, connecting_devices_))
return;
UpdateClickedDevice(device_address, container);
LogUserBluetoothEvent(device_address);
helper->ConnectToBluetoothDevice(device_address);
}
void BluetoothDetailedView::CreateExtraTitleRowButtons() {
if (login_ == LoginStatus::LOCKED)
return;
DCHECK(!toggle_);
DCHECK(!settings_);
tri_view()->SetContainerVisible(TriView::Container::END, true);
toggle_ = TrayPopupUtils::CreateToggleButton(
base::BindRepeating(&BluetoothDetailedView::ToggleButtonPressed,
base::Unretained(this)),
IDS_ASH_STATUS_TRAY_BLUETOOTH);
toggle_->SetIsOn(Shell::Get()->tray_bluetooth_helper()->GetBluetoothState() ==
BluetoothSystem::State::kPoweredOn);
tri_view()->AddView(TriView::Container::END, toggle_);
settings_ = CreateSettingsButton(
base::BindRepeating(&BluetoothDetailedView::ShowSettings,
base::Unretained(this)),
IDS_ASH_STATUS_TRAY_BLUETOOTH_SETTINGS);
tri_view()->AddView(TriView::Container::END, settings_);
}
} // namespace tray
} // namespace ash