1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ash/system/bluetooth/tray_bluetooth.h"
6
7#include "ash/session/session_state_delegate.h"
8#include "ash/shell.h"
9#include "ash/system/tray/fixed_sized_scroll_view.h"
10#include "ash/system/tray/hover_highlight_view.h"
11#include "ash/system/tray/system_tray.h"
12#include "ash/system/tray/system_tray_delegate.h"
13#include "ash/system/tray/system_tray_notifier.h"
14#include "ash/system/tray/throbber_view.h"
15#include "ash/system/tray/tray_constants.h"
16#include "ash/system/tray/tray_details_view.h"
17#include "ash/system/tray/tray_item_more.h"
18#include "ash/system/tray/tray_popup_header_button.h"
19#include "grit/ash_resources.h"
20#include "grit/ash_strings.h"
21#include "ui/base/l10n/l10n_util.h"
22#include "ui/base/resource/resource_bundle.h"
23#include "ui/gfx/image/image.h"
24#include "ui/views/controls/image_view.h"
25#include "ui/views/controls/label.h"
26#include "ui/views/layout/box_layout.h"
27
28namespace ash {
29namespace tray {
30namespace {
31
32// Updates bluetooth device |device| in the |list|. If it is new, append to the
33// end of the |list|; otherwise, keep it at the same place, but update the data
34// with new device info provided by |device|.
35void UpdateBluetoothDeviceListHelper(BluetoothDeviceList* list,
36                                     const BluetoothDeviceInfo& device) {
37  for (BluetoothDeviceList::iterator it = list->begin(); it != list->end();
38       ++it) {
39    if ((*it).address == device.address) {
40      *it = device;
41      return;
42    }
43  }
44
45  list->push_back(device);
46}
47
48// Removes the obsolete BluetoothDevices from |list|, if they are not in the
49// |new_list|.
50void RemoveObsoleteBluetoothDevicesFromList(
51    BluetoothDeviceList* list,
52    const std::set<std::string>& new_list) {
53  for (BluetoothDeviceList::iterator it = list->begin(); it != list->end();
54       ++it) {
55    if (new_list.find((*it).address) == new_list.end()) {
56      it = list->erase(it);
57      if (it == list->end())
58        return;
59    }
60  }
61}
62
63}  // namespace
64
65class BluetoothDefaultView : public TrayItemMore {
66 public:
67  BluetoothDefaultView(SystemTrayItem* owner, bool show_more)
68      : TrayItemMore(owner, show_more) {
69    ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
70    SetImage(bundle.GetImageNamed(IDR_AURA_UBER_TRAY_BLUETOOTH).ToImageSkia());
71    UpdateLabel();
72  }
73
74  virtual ~BluetoothDefaultView() {}
75
76  void UpdateLabel() {
77    ash::SystemTrayDelegate* delegate =
78        ash::Shell::GetInstance()->system_tray_delegate();
79    if (delegate->GetBluetoothAvailable()) {
80      ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
81      const base::string16 label =
82          rb.GetLocalizedString(delegate->GetBluetoothEnabled() ?
83              IDS_ASH_STATUS_TRAY_BLUETOOTH_ENABLED :
84              IDS_ASH_STATUS_TRAY_BLUETOOTH_DISABLED);
85      SetLabel(label);
86      SetAccessibleName(label);
87      SetVisible(true);
88    } else {
89      SetVisible(false);
90    }
91  }
92
93 private:
94  DISALLOW_COPY_AND_ASSIGN(BluetoothDefaultView);
95};
96
97class BluetoothDetailedView : public TrayDetailsView,
98                              public ViewClickListener,
99                              public views::ButtonListener {
100 public:
101  BluetoothDetailedView(SystemTrayItem* owner, user::LoginStatus login)
102      : TrayDetailsView(owner),
103        login_(login),
104        manage_devices_(NULL),
105        toggle_bluetooth_(NULL),
106        enable_bluetooth_(NULL) {
107    CreateItems();
108  }
109
110  virtual ~BluetoothDetailedView() {
111    // Stop discovering bluetooth devices when exiting BT detailed view.
112    BluetoothStopDiscovering();
113  }
114
115  void Update() {
116    BluetoothStartDiscovering();
117    UpdateBluetoothDeviceList();
118
119    // Update UI.
120    UpdateDeviceScrollList();
121    UpdateHeaderEntry();
122    Layout();
123  }
124
125 private:
126  void CreateItems() {
127    CreateScrollableList();
128    AppendSettingsEntries();
129    AppendHeaderEntry();
130  }
131
132  void BluetoothStartDiscovering() {
133    ash::SystemTrayDelegate* delegate =
134        ash::Shell::GetInstance()->system_tray_delegate();
135    bool bluetooth_enabled = delegate->GetBluetoothEnabled();
136    bool bluetooth_discovering = delegate->GetBluetoothDiscovering();
137    if (bluetooth_discovering) {
138      throbber_->Start();
139      return;
140    }
141    throbber_->Stop();
142    if (bluetooth_enabled) {
143      delegate->BluetoothStartDiscovering();
144    }
145  }
146
147  void BluetoothStopDiscovering() {
148    ash::SystemTrayDelegate* delegate =
149        ash::Shell::GetInstance()->system_tray_delegate();
150    if (delegate && delegate->GetBluetoothDiscovering()) {
151      delegate->BluetoothStopDiscovering();
152      throbber_->Stop();
153    }
154  }
155
156  void UpdateBluetoothDeviceList() {
157    std::set<std::string> new_connecting_devices;
158    std::set<std::string> new_connected_devices;
159    std::set<std::string> new_paired_not_connected_devices;
160    std::set<std::string> new_discovered_not_paired_devices;
161
162    BluetoothDeviceList list;
163    Shell::GetInstance()->system_tray_delegate()->
164        GetAvailableBluetoothDevices(&list);
165    for (size_t i = 0; i < list.size(); ++i) {
166      if (list[i].connecting) {
167        list[i].display_name = l10n_util::GetStringFUTF16(
168            IDS_ASH_STATUS_TRAY_BLUETOOTH_CONNECTING, list[i].display_name);
169        new_connecting_devices.insert(list[i].address);
170        UpdateBluetoothDeviceListHelper(&connecting_devices_, list[i]);
171      } else if (list[i].connected && list[i].paired) {
172        new_connected_devices.insert(list[i].address);
173        UpdateBluetoothDeviceListHelper(&connected_devices_, list[i]);
174      } else if (list[i].paired) {
175        new_paired_not_connected_devices.insert(list[i].address);
176        UpdateBluetoothDeviceListHelper(
177            &paired_not_connected_devices_, list[i]);
178      } else {
179        new_discovered_not_paired_devices.insert(list[i].address);
180        UpdateBluetoothDeviceListHelper(
181            &discovered_not_paired_devices_, list[i]);
182      }
183    }
184    RemoveObsoleteBluetoothDevicesFromList(&connecting_devices_,
185                                           new_connecting_devices);
186    RemoveObsoleteBluetoothDevicesFromList(&connected_devices_,
187                                           new_connected_devices);
188    RemoveObsoleteBluetoothDevicesFromList(&paired_not_connected_devices_,
189                                           new_paired_not_connected_devices);
190    RemoveObsoleteBluetoothDevicesFromList(&discovered_not_paired_devices_,
191                                           new_discovered_not_paired_devices);
192  }
193
194  void AppendHeaderEntry() {
195    CreateSpecialRow(IDS_ASH_STATUS_TRAY_BLUETOOTH, this);
196
197    if (login_ == user::LOGGED_IN_LOCKED)
198      return;
199
200    throbber_ = new ThrobberView;
201    throbber_->SetTooltipText(
202        l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERING));
203    footer()->AddThrobber(throbber_);
204
205    // Do not allow toggling bluetooth in the lock screen.
206    ash::SystemTrayDelegate* delegate =
207        ash::Shell::GetInstance()->system_tray_delegate();
208    toggle_bluetooth_ = new TrayPopupHeaderButton(this,
209        IDR_AURA_UBER_TRAY_BLUETOOTH_ENABLED,
210        IDR_AURA_UBER_TRAY_BLUETOOTH_DISABLED,
211        IDR_AURA_UBER_TRAY_BLUETOOTH_ENABLED_HOVER,
212        IDR_AURA_UBER_TRAY_BLUETOOTH_DISABLED_HOVER,
213        IDS_ASH_STATUS_TRAY_BLUETOOTH);
214    toggle_bluetooth_->SetToggled(!delegate->GetBluetoothEnabled());
215    toggle_bluetooth_->SetTooltipText(
216        l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_DISABLE_BLUETOOTH));
217    toggle_bluetooth_->SetToggledTooltipText(
218        l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_ENABLE_BLUETOOTH));
219    footer()->AddButton(toggle_bluetooth_);
220 }
221
222  void UpdateHeaderEntry() {
223    if (toggle_bluetooth_) {
224      toggle_bluetooth_->SetToggled(
225          !ash::Shell::GetInstance()->system_tray_delegate()->
226              GetBluetoothEnabled());
227    }
228  }
229
230  void UpdateDeviceScrollList() {
231    device_map_.clear();
232    scroll_content()->RemoveAllChildViews(true);
233    enable_bluetooth_ = NULL;
234
235    ash::SystemTrayDelegate* delegate =
236        ash::Shell::GetInstance()->system_tray_delegate();
237    bool bluetooth_enabled = delegate->GetBluetoothEnabled();
238    bool blueooth_available = delegate->GetBluetoothAvailable();
239    if (blueooth_available && !bluetooth_enabled &&
240        toggle_bluetooth_) {
241      enable_bluetooth_ =
242          AddScrollListItem(
243              l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_ENABLE_BLUETOOTH),
244              gfx::Font::NORMAL, false, true);
245    }
246
247    AppendSameTypeDevicesToScrollList(
248        connected_devices_, true, true, bluetooth_enabled);
249    AppendSameTypeDevicesToScrollList(
250        connecting_devices_, true, false, bluetooth_enabled);
251    AppendSameTypeDevicesToScrollList(
252        paired_not_connected_devices_, false, false, bluetooth_enabled);
253    if (discovered_not_paired_devices_.size() > 0)
254      AddScrollSeparator();
255    AppendSameTypeDevicesToScrollList(
256        discovered_not_paired_devices_, false, false, bluetooth_enabled);
257
258    // Show user Bluetooth state if there is no bluetooth devices in list.
259    if (device_map_.size() == 0) {
260      if (blueooth_available && bluetooth_enabled) {
261        AddScrollListItem(
262            l10n_util::GetStringUTF16(
263                IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERING),
264            gfx::Font::NORMAL, false, true);
265      }
266    }
267
268    scroll_content()->SizeToPreferredSize();
269    static_cast<views::View*>(scroller())->Layout();
270  }
271
272  void AppendSameTypeDevicesToScrollList(const BluetoothDeviceList& list,
273                                         bool bold,
274                                         bool checked,
275                                         bool enabled) {
276    for (size_t i = 0; i < list.size(); ++i) {
277      HoverHighlightView* container = AddScrollListItem(
278          list[i].display_name,
279          bold? gfx::Font::BOLD : gfx::Font::NORMAL,
280          checked, enabled);
281      device_map_[container] = list[i].address;
282    }
283  }
284
285  HoverHighlightView* AddScrollListItem(const base::string16& text,
286                                        gfx::Font::FontStyle style,
287                                        bool checked,
288                                        bool enabled) {
289    HoverHighlightView* container = new HoverHighlightView(this);
290    views::Label* label = container->AddCheckableLabel(text, style, checked);
291    label->SetEnabled(enabled);
292    scroll_content()->AddChildView(container);
293    return container;
294  }
295
296  // Add settings entries.
297  void AppendSettingsEntries() {
298    if (!ash::Shell::GetInstance()->
299            system_tray_delegate()->ShouldShowSettings()) {
300      return;
301    }
302
303    // Add bluetooth device requires a browser window, hide it for non logged in
304    // user.
305    bool userAddingRunning = ash::Shell::GetInstance()
306                                 ->session_state_delegate()
307                                 ->IsInSecondaryLoginScreen();
308
309    if (login_ == user::LOGGED_IN_NONE || login_ == user::LOGGED_IN_LOCKED ||
310        userAddingRunning)
311      return;
312
313    ash::SystemTrayDelegate* delegate =
314        ash::Shell::GetInstance()->system_tray_delegate();
315    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
316    HoverHighlightView* container = new HoverHighlightView(this);
317    container->AddLabel(
318        rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_BLUETOOTH_MANAGE_DEVICES),
319        gfx::ALIGN_LEFT,
320        gfx::Font::NORMAL);
321    container->SetEnabled(delegate->GetBluetoothAvailable());
322    AddChildView(container);
323    manage_devices_ = container;
324  }
325
326  // Returns true if the device with |device_id| is found in |device_list|,
327  // and the display_name of the device will be returned in |display_name| if
328  // it's not NULL.
329  bool FoundDevice(const std::string& device_id,
330                   const BluetoothDeviceList& device_list,
331                   base::string16* display_name) {
332    for (size_t i = 0; i < device_list.size(); ++i) {
333      if (device_list[i].address == device_id) {
334        if (display_name)
335          *display_name = device_list[i].display_name;
336        return true;
337      }
338    }
339    return false;
340  }
341
342  // Updates UI of the clicked bluetooth device to show it is being connected
343  // or disconnected if such an operation is going to be performed underway.
344  void UpdateClickedDevice(std::string device_id, views::View* item_container) {
345    base::string16 display_name;
346    if (FoundDevice(device_id, paired_not_connected_devices_,
347                           &display_name)) {
348      display_name = l10n_util::GetStringFUTF16(
349          IDS_ASH_STATUS_TRAY_BLUETOOTH_CONNECTING, display_name);
350
351      item_container->RemoveAllChildViews(true);
352      static_cast<HoverHighlightView*>(item_container)->
353          AddCheckableLabel(display_name, gfx::Font::BOLD, false);
354      scroll_content()->SizeToPreferredSize();
355      static_cast<views::View*>(scroller())->Layout();
356    }
357  }
358
359  // Overridden from ViewClickListener.
360  virtual void OnViewClicked(views::View* sender) OVERRIDE {
361    ash::SystemTrayDelegate* delegate =
362        ash::Shell::GetInstance()->system_tray_delegate();
363    if (sender == footer()->content()) {
364      TransitionToDefaultView();
365    } else if (sender == manage_devices_) {
366      delegate->ManageBluetoothDevices();
367    } else if (sender == enable_bluetooth_) {
368      Shell::GetInstance()->metrics()->RecordUserMetricsAction(
369          delegate->GetBluetoothEnabled() ?
370          ash::UMA_STATUS_AREA_BLUETOOTH_DISABLED :
371          ash::UMA_STATUS_AREA_BLUETOOTH_ENABLED);
372      delegate->ToggleBluetooth();
373    } else {
374      if (!delegate->GetBluetoothEnabled())
375        return;
376      std::map<views::View*, std::string>::iterator find;
377      find = device_map_.find(sender);
378      if (find == device_map_.end())
379        return;
380      std::string device_id = find->second;
381      if (FoundDevice(device_id, connecting_devices_, NULL))
382        return;
383      UpdateClickedDevice(device_id, sender);
384      delegate->ConnectToBluetoothDevice(device_id);
385    }
386  }
387
388  // Overridden from ButtonListener.
389  virtual void ButtonPressed(views::Button* sender,
390                             const ui::Event& event) OVERRIDE {
391    ash::SystemTrayDelegate* delegate =
392        ash::Shell::GetInstance()->system_tray_delegate();
393    if (sender == toggle_bluetooth_)
394      delegate->ToggleBluetooth();
395    else
396      NOTREACHED();
397  }
398
399  user::LoginStatus login_;
400
401  std::map<views::View*, std::string> device_map_;
402  views::View* manage_devices_;
403  ThrobberView* throbber_;
404  TrayPopupHeaderButton* toggle_bluetooth_;
405  HoverHighlightView* enable_bluetooth_;
406  BluetoothDeviceList connected_devices_;
407  BluetoothDeviceList connecting_devices_;
408  BluetoothDeviceList paired_not_connected_devices_;
409  BluetoothDeviceList discovered_not_paired_devices_;
410
411  DISALLOW_COPY_AND_ASSIGN(BluetoothDetailedView);
412};
413
414}  // namespace tray
415
416TrayBluetooth::TrayBluetooth(SystemTray* system_tray)
417    : SystemTrayItem(system_tray),
418      default_(NULL),
419      detailed_(NULL) {
420  Shell::GetInstance()->system_tray_notifier()->AddBluetoothObserver(this);
421}
422
423TrayBluetooth::~TrayBluetooth() {
424  Shell::GetInstance()->system_tray_notifier()->RemoveBluetoothObserver(this);
425}
426
427views::View* TrayBluetooth::CreateTrayView(user::LoginStatus status) {
428  return NULL;
429}
430
431views::View* TrayBluetooth::CreateDefaultView(user::LoginStatus status) {
432  CHECK(default_ == NULL);
433  default_ = new tray::BluetoothDefaultView(
434      this, status != user::LOGGED_IN_LOCKED);
435  return default_;
436}
437
438views::View* TrayBluetooth::CreateDetailedView(user::LoginStatus status) {
439  if (!Shell::GetInstance()->system_tray_delegate()->GetBluetoothAvailable())
440    return NULL;
441  Shell::GetInstance()->metrics()->RecordUserMetricsAction(
442      ash::UMA_STATUS_AREA_DETAILED_BLUETOOTH_VIEW);
443  CHECK(detailed_ == NULL);
444  detailed_ = new tray::BluetoothDetailedView(this, status);
445  detailed_->Update();
446  return detailed_;
447}
448
449void TrayBluetooth::DestroyTrayView() {
450}
451
452void TrayBluetooth::DestroyDefaultView() {
453  default_ = NULL;
454}
455
456void TrayBluetooth::DestroyDetailedView() {
457  detailed_ = NULL;
458}
459
460void TrayBluetooth::UpdateAfterLoginStatusChange(user::LoginStatus status) {
461}
462
463void TrayBluetooth::OnBluetoothRefresh() {
464  if (default_)
465    default_->UpdateLabel();
466  else if (detailed_)
467    detailed_->Update();
468}
469
470void TrayBluetooth::OnBluetoothDiscoveringChanged() {
471  if (!detailed_)
472    return;
473  detailed_->Update();
474}
475
476}  // namespace ash
477