| // 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 |