blob: 099fa11c0bed8bdd3187c4f29c927b8fca8f7dc9 [file] [log] [blame]
// Copyright (c) 2012 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/tray_bluetooth.h"
#include <map>
#include <memory>
#include <set>
#include <string>
#include "ash/ash_view_ids.h"
#include "ash/metrics/user_metrics_recorder.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/bluetooth/bluetooth_power_controller.h"
#include "ash/system/bluetooth/tray_bluetooth_helper.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/system_tray.h"
#include "ash/system/tray/system_tray_controller.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_details_view.h"
#include "ash/system/tray/tray_info_label.h"
#include "ash/system/tray/tray_item_more.h"
#include "ash/system/tray/tray_popup_item_style.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tri_view.h"
#include "device/bluetooth/bluetooth_common.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
namespace ash {
namespace tray {
namespace {
const int kUpdateFrequencyMs = 1000;
// Updates bluetooth device |device| in the |list|. If it is new, append to the
// end of the |list|; otherwise, keep it at the same place, but update the data
// with new device info provided by |device|.
void UpdateBluetoothDeviceListHelper(BluetoothDeviceList* list,
const BluetoothDeviceInfo& device) {
for (BluetoothDeviceList::iterator it = list->begin(); it != list->end();
++it) {
if ((*it).address == device.address) {
*it = device;
return;
}
}
list->push_back(device);
}
// Removes the obsolete BluetoothDevices from |list|, if they are not in the
// |new_list|.
void RemoveObsoleteBluetoothDevicesFromList(
BluetoothDeviceList* list,
const std::set<std::string>& new_list) {
for (BluetoothDeviceList::iterator it = list->begin(); it != list->end();
++it) {
if (new_list.find((*it).address) == new_list.end()) {
it = list->erase(it);
if (it == list->end())
return;
}
}
}
// Returns corresponding device type icons for given Bluetooth device types and
// connection states.
const gfx::VectorIcon& GetBluetoothDeviceIcon(
device::BluetoothDeviceType device_type,
bool connected) {
switch (device_type) {
case device::BluetoothDeviceType::COMPUTER:
return ash::kSystemMenuComputerIcon;
case device::BluetoothDeviceType::PHONE:
return ash::kSystemMenuPhoneIcon;
case device::BluetoothDeviceType::AUDIO:
case device::BluetoothDeviceType::CAR_AUDIO:
return ash::kSystemMenuHeadsetIcon;
case device::BluetoothDeviceType::VIDEO:
return ash::kSystemMenuVideocamIcon;
case device::BluetoothDeviceType::JOYSTICK:
case device::BluetoothDeviceType::GAMEPAD:
return ash::kSystemMenuGamepadIcon;
case device::BluetoothDeviceType::KEYBOARD:
case device::BluetoothDeviceType::KEYBOARD_MOUSE_COMBO:
return ash::kSystemMenuKeyboardIcon;
case device::BluetoothDeviceType::TABLET:
return ash::kSystemMenuTabletIcon;
case device::BluetoothDeviceType::MOUSE:
return ash::kSystemMenuMouseIcon;
case device::BluetoothDeviceType::MODEM:
case device::BluetoothDeviceType::PERIPHERAL:
return ash::kSystemMenuBluetoothIcon;
default:
return connected ? ash::kSystemMenuBluetoothConnectedIcon
: ash::kSystemMenuBluetoothIcon;
}
}
const int kDisabledPanelLabelBaselineY = 20;
} // namespace
class BluetoothDefaultView : public TrayItemMore {
public:
explicit BluetoothDefaultView(SystemTrayItem* owner) : TrayItemMore(owner) {
set_id(VIEW_ID_BLUETOOTH_DEFAULT_VIEW);
}
~BluetoothDefaultView() override = default;
void Update() {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
if (helper->GetBluetoothAvailable()) {
const base::string16 label = l10n_util::GetStringUTF16(
helper->GetBluetoothEnabled()
? IDS_ASH_STATUS_TRAY_BLUETOOTH_ENABLED
: IDS_ASH_STATUS_TRAY_BLUETOOTH_DISABLED);
SetLabel(label);
SetAccessibleName(label);
SetVisible(true);
} else {
SetVisible(false);
}
UpdateStyle();
}
protected:
// TrayItemMore:
std::unique_ptr<TrayPopupItemStyle> HandleCreateStyle() const override {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
std::unique_ptr<TrayPopupItemStyle> style =
TrayItemMore::HandleCreateStyle();
style->set_color_style(
helper->GetBluetoothEnabled()
? TrayPopupItemStyle::ColorStyle::ACTIVE
: helper->GetBluetoothAvailable()
? TrayPopupItemStyle::ColorStyle::INACTIVE
: TrayPopupItemStyle::ColorStyle::DISABLED);
return style;
}
void UpdateStyle() override {
TrayItemMore::UpdateStyle();
std::unique_ptr<TrayPopupItemStyle> style = CreateStyle();
SetImage(gfx::CreateVectorIcon(GetCurrentIcon(), style->GetIconColor()));
}
private:
const gfx::VectorIcon& GetCurrentIcon() {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
if (!helper->GetBluetoothEnabled())
return kSystemMenuBluetoothDisabledIcon;
bool has_connected_device = false;
BluetoothDeviceList list = helper->GetAvailableBluetoothDevices();
for (const auto& device : list) {
if (device.connected) {
has_connected_device = true;
break;
}
}
return has_connected_device ? kSystemMenuBluetoothConnectedIcon
: kSystemMenuBluetoothIcon;
}
DISALLOW_COPY_AND_ASSIGN(BluetoothDefaultView);
};
class BluetoothDetailedView : public TrayDetailsView {
public:
BluetoothDetailedView(SystemTrayItem* owner, LoginStatus login)
: TrayDetailsView(owner),
login_(login),
toggle_(nullptr),
settings_(nullptr),
disabled_panel_(nullptr) {
CreateItems();
}
~BluetoothDetailedView() override {
// Stop discovering bluetooth devices when exiting BT detailed view.
BluetoothStopDiscovering();
}
void Update() {
// Update immediately for initial device list and
// when bluetooth is disabled.
if (device_map_.size() == 0 ||
!Shell::Get()->tray_bluetooth_helper()->GetBluetoothEnabled()) {
DoUpdate();
return;
}
// Return here since an update is already queued.
if (timer_.IsRunning())
return;
// Update the detailed view after kUpdateFrequencyMs.
timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(kUpdateFrequencyMs), this,
&BluetoothDetailedView::DoUpdate);
}
private:
void CreateItems() {
CreateScrollableList();
CreateTitleRow(IDS_ASH_STATUS_TRAY_BLUETOOTH);
}
void BluetoothStartDiscovering() {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
if (helper->HasBluetoothDiscoverySession()) {
ShowLoadingIndicator();
return;
}
HideLoadingIndicator();
if (helper->GetBluetoothEnabled())
helper->StartBluetoothDiscovering();
}
void BluetoothStopDiscovering() {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
if (helper && helper->HasBluetoothDiscoverySession()) {
helper->StopBluetoothDiscovering();
HideLoadingIndicator();
}
}
void UpdateBluetoothDeviceList() {
std::set<std::string> new_connecting_devices;
std::set<std::string> new_connected_devices;
std::set<std::string> new_paired_not_connected_devices;
std::set<std::string> new_discovered_not_paired_devices;
BluetoothDeviceList list =
Shell::Get()->tray_bluetooth_helper()->GetAvailableBluetoothDevices();
for (const auto& device : list) {
if (device.connecting) {
new_connecting_devices.insert(device.address);
UpdateBluetoothDeviceListHelper(&connecting_devices_, device);
} else if (device.connected && device.paired) {
new_connected_devices.insert(device.address);
UpdateBluetoothDeviceListHelper(&connected_devices_, device);
} else if (device.paired) {
new_paired_not_connected_devices.insert(device.address);
UpdateBluetoothDeviceListHelper(&paired_not_connected_devices_, device);
} else {
new_discovered_not_paired_devices.insert(device.address);
UpdateBluetoothDeviceListHelper(&discovered_not_paired_devices_,
device);
}
}
RemoveObsoleteBluetoothDevicesFromList(&connecting_devices_,
new_connecting_devices);
RemoveObsoleteBluetoothDevicesFromList(&connected_devices_,
new_connected_devices);
RemoveObsoleteBluetoothDevicesFromList(&paired_not_connected_devices_,
new_paired_not_connected_devices);
RemoveObsoleteBluetoothDevicesFromList(&discovered_not_paired_devices_,
new_discovered_not_paired_devices);
}
void UpdateHeaderEntry() {
const bool is_bluetooth_enabled =
Shell::Get()->tray_bluetooth_helper()->GetBluetoothEnabled();
if (toggle_)
toggle_->SetIsOn(is_bluetooth_enabled, true);
}
void UpdateDeviceScrollList() {
std::string focused_device_address = GetFocusedDeviceAddress();
device_map_.clear();
scroll_content()->RemoveAllChildViews(true);
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
const bool bluetooth_enabled = helper->GetBluetoothEnabled();
const bool bluetooth_available = helper->GetBluetoothAvailable();
// If Bluetooth is disabled, show a panel which only indicates that it is
// disabled, instead of the scroller with Bluetooth devices.
if (bluetooth_enabled) {
HideDisabledPanel();
} else {
ShowDisabledPanel();
return;
}
// Add paired devices and their section header to the list.
size_t num_paired_devices = connected_devices_.size() +
connecting_devices_.size() +
paired_not_connected_devices_.size();
if (num_paired_devices > 0) {
AddScrollListSubHeader(IDS_ASH_STATUS_TRAY_BLUETOOTH_PAIRED_DEVICES);
AppendSameTypeDevicesToScrollList(connected_devices_, true, true,
bluetooth_enabled);
AppendSameTypeDevicesToScrollList(connecting_devices_, true, false,
bluetooth_enabled);
AppendSameTypeDevicesToScrollList(paired_not_connected_devices_, false,
false, bluetooth_enabled);
}
// 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_.size() > 0) {
if (num_paired_devices > 0)
AddScrollListSubHeader(IDS_ASH_STATUS_TRAY_BLUETOOTH_UNPAIRED_DEVICES);
AppendSameTypeDevicesToScrollList(discovered_not_paired_devices_, false,
false, bluetooth_enabled);
}
// Show user Bluetooth state if there is no bluetooth devices in list.
if (device_map_.size() == 0 && bluetooth_available && bluetooth_enabled) {
scroll_content()->AddChildView(new TrayInfoLabel(
nullptr /* delegate */, IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERING));
}
// Focus the device which was focused before the device-list update.
if (!focused_device_address.empty())
FocusDeviceByAddress(focused_device_address);
scroll_content()->InvalidateLayout();
}
void AppendSameTypeDevicesToScrollList(const BluetoothDeviceList& list,
bool highlight,
bool checked,
bool enabled) {
for (const auto& device : list) {
const gfx::VectorIcon& icon =
GetBluetoothDeviceIcon(device.device_type, device.connected);
HoverHighlightView* container =
AddScrollListItem(icon, device.display_name);
if (device.connected)
SetupConnectedScrollListItem(container);
else if (device.connecting)
SetupConnectingScrollListItem(container);
device_map_[container] = device.address;
}
}
// Returns true if the device with |device_id| is found in |device_list|.
bool FoundDevice(const std::string& device_id,
const BluetoothDeviceList& device_list) {
for (const auto& device : device_list) {
if (device.address == device_id)
return true;
}
return false;
}
// Updates UI of the clicked bluetooth device to show it is being connected
// or disconnected if such an operation is going to be performed underway.
void UpdateClickedDevice(const std::string& device_id,
views::View* item_container) {
if (FoundDevice(device_id, paired_not_connected_devices_)) {
HoverHighlightView* container =
static_cast<HoverHighlightView*>(item_container);
SetupConnectingScrollListItem(container);
scroll_content()->SizeToPreferredSize();
scroller()->Layout();
}
}
// TrayDetailsView:
void HandleViewClicked(views::View* view) override {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
if (!helper->GetBluetoothEnabled())
return;
std::map<views::View*, std::string>::iterator find;
find = device_map_.find(view);
if (find == device_map_.end())
return;
const std::string device_id = find->second;
if (FoundDevice(device_id, connecting_devices_))
return;
UpdateClickedDevice(device_id, view);
helper->ConnectToBluetoothDevice(device_id);
}
void HandleButtonPressed(views::Button* sender,
const ui::Event& event) override {
if (sender == toggle_) {
TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
BluetoothPowerController* power_controller =
Shell::Get()->bluetooth_power_controller();
Shell::Get()->metrics()->RecordUserMetricsAction(
helper->GetBluetoothEnabled() ? UMA_STATUS_AREA_BLUETOOTH_DISABLED
: UMA_STATUS_AREA_BLUETOOTH_ENABLED);
power_controller->ToggleBluetoothEnabled();
} else if (sender == settings_) {
ShowSettings();
} else {
NOTREACHED();
}
}
void CreateExtraTitleRowButtons() override {
if (login_ == LoginStatus::LOCKED)
return;
DCHECK(!toggle_);
DCHECK(!settings_);
tri_view()->SetContainerVisible(TriView::Container::END, true);
toggle_ =
TrayPopupUtils::CreateToggleButton(this, IDS_ASH_STATUS_TRAY_BLUETOOTH);
tri_view()->AddView(TriView::Container::END, toggle_);
settings_ = CreateSettingsButton(IDS_ASH_STATUS_TRAY_BLUETOOTH_SETTINGS);
tri_view()->AddView(TriView::Container::END, settings_);
}
void ShowSettings() {
if (TrayPopupUtils::CanOpenWebUISettings()) {
Shell::Get()->system_tray_controller()->ShowBluetoothSettings();
owner()->system_tray()->CloseBubble();
}
}
void ShowLoadingIndicator() {
// Setting a value of -1 gives progress_bar an infinite-loading behavior.
ShowProgress(-1, true);
}
void HideLoadingIndicator() { ShowProgress(0, false); }
void ShowDisabledPanel() {
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);
}
void HideDisabledPanel() {
DCHECK(scroller());
if (disabled_panel_)
disabled_panel_->SetVisible(false);
scroller()->SetVisible(true);
}
views::View* CreateDisabledPanel() {
views::View* container = new views::View;
auto box_layout =
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical);
box_layout->set_main_axis_alignment(
views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER);
container->SetLayoutManager(std::move(box_layout));
TrayPopupItemStyle style(
TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL);
style.set_color_style(TrayPopupItemStyle::ColorStyle::DISABLED);
views::ImageView* image_view = new views::ImageView;
image_view->SetImage(gfx::CreateVectorIcon(kSystemMenuBluetoothDisabledIcon,
style.GetIconColor()));
image_view->SetVerticalAlignment(views::ImageView::TRAILING);
container->AddChildView(image_view);
views::Label* label = new views::Label(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISABLED));
style.SetupLabel(label);
label->SetBorder(views::CreateEmptyBorder(
kDisabledPanelLabelBaselineY - label->GetBaseline(), 0, 0, 0));
container->AddChildView(label);
// 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;
}
std::string GetFocusedDeviceAddress() {
for (auto& view_and_address : device_map_) {
if (view_and_address.first->HasFocus())
return view_and_address.second;
}
return std::string();
}
void FocusDeviceByAddress(const std::string& address) {
for (auto& view_and_address : device_map_) {
if (view_and_address.second == address) {
view_and_address.first->RequestFocus();
return;
}
}
}
void DoUpdate() {
BluetoothStartDiscovering();
UpdateBluetoothDeviceList();
// Update UI.
UpdateDeviceScrollList();
UpdateHeaderEntry();
Layout();
}
// TODO(jamescook): Don't cache this.
LoginStatus login_;
std::map<views::View*, std::string> device_map_;
BluetoothDeviceList connected_devices_;
BluetoothDeviceList connecting_devices_;
BluetoothDeviceList paired_not_connected_devices_;
BluetoothDeviceList discovered_not_paired_devices_;
views::ToggleButton* toggle_;
views::Button* settings_;
// The container of the message "Bluetooth is disabled" and an icon. It should
// be shown instead of Bluetooth device list when Bluetooth is disabled.
views::View* disabled_panel_;
// Timer used to limit the update frequency.
base::OneShotTimer timer_;
DISALLOW_COPY_AND_ASSIGN(BluetoothDetailedView);
};
} // namespace tray
TrayBluetooth::TrayBluetooth(SystemTray* system_tray)
: SystemTrayItem(system_tray, UMA_BLUETOOTH),
default_(nullptr),
detailed_(nullptr) {
Shell::Get()->system_tray_notifier()->AddBluetoothObserver(this);
}
TrayBluetooth::~TrayBluetooth() {
Shell::Get()->system_tray_notifier()->RemoveBluetoothObserver(this);
}
views::View* TrayBluetooth::CreateDefaultView(LoginStatus status) {
CHECK(default_ == nullptr);
SessionController* session_controller = Shell::Get()->session_controller();
default_ = new tray::BluetoothDefaultView(this);
if (!session_controller->IsActiveUserSessionStarted()) {
// Bluetooth power setting is always mutable in login screen before any
// user logs in. The changes will affect local state preferences.
default_->SetEnabled(true);
} else {
// The bluetooth setting should be mutable only if:
// * the active user is the primary user, and
// * the session is not in lock screen
// The changes will affect the primary user's preferences.
default_->SetEnabled(session_controller->IsUserPrimary() &&
status != LoginStatus::LOCKED);
}
default_->Update();
return default_;
}
views::View* TrayBluetooth::CreateDetailedView(LoginStatus status) {
if (!Shell::Get()->tray_bluetooth_helper()->GetBluetoothAvailable())
return nullptr;
Shell::Get()->metrics()->RecordUserMetricsAction(
UMA_STATUS_AREA_DETAILED_BLUETOOTH_VIEW);
CHECK(detailed_ == nullptr);
detailed_ = new tray::BluetoothDetailedView(this, status);
detailed_->Update();
return detailed_;
}
void TrayBluetooth::OnDefaultViewDestroyed() {
default_ = nullptr;
}
void TrayBluetooth::OnDetailedViewDestroyed() {
detailed_ = nullptr;
}
void TrayBluetooth::UpdateAfterLoginStatusChange(LoginStatus status) {}
void TrayBluetooth::OnBluetoothRefresh() {
if (default_)
default_->Update();
else if (detailed_)
detailed_->Update();
}
void TrayBluetooth::OnBluetoothDiscoveringChanged() {
if (!detailed_)
return;
detailed_->Update();
}
} // namespace ash