bluetooth_detailed_view_legacy.cc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. // Copyright 2018 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. #include "ash/system/bluetooth/bluetooth_detailed_view_legacy.h"
  5. #include <memory>
  6. #include <string>
  7. #include <unordered_map>
  8. #include <utility>
  9. #include "ash/public/cpp/system_tray_client.h"
  10. #include "ash/resources/vector_icons/vector_icons.h"
  11. #include "ash/shell.h"
  12. #include "ash/strings/grit/ash_strings.h"
  13. #include "ash/style/ash_color_provider.h"
  14. #include "ash/system/machine_learning/user_settings_event_logger.h"
  15. #include "ash/system/model/system_tray_model.h"
  16. #include "ash/system/tray/hover_highlight_view.h"
  17. #include "ash/system/tray/tray_info_label.h"
  18. #include "ash/system/tray/tray_popup_utils.h"
  19. #include "ash/system/tray/tray_toggle_button.h"
  20. #include "ash/system/tray/tray_utils.h"
  21. #include "base/bind.h"
  22. #include "base/strings/utf_string_conversions.h"
  23. #include "chromeos/ui/vector_icons/vector_icons.h"
  24. #include "device/bluetooth/chromeos/bluetooth_utils.h"
  25. #include "services/device/public/cpp/bluetooth/bluetooth_utils.h"
  26. #include "ui/base/l10n/l10n_util.h"
  27. #include "ui/gfx/paint_vector_icon.h"
  28. #include "ui/views/border.h"
  29. #include "ui/views/controls/button/toggle_button.h"
  30. #include "ui/views/controls/image_view.h"
  31. #include "ui/views/controls/scroll_view.h"
  32. #include "ui/views/layout/box_layout.h"
  33. #include "ui/views/view_utils.h"
  34. using device::mojom::BluetoothDeviceInfo;
  35. using device::mojom::BluetoothSystem;
  36. namespace ash {
  37. namespace {
  38. const int kDisabledPanelLabelBaselineY = 20;
  39. const int kEnterpriseManagedIconSizeDip = 20;
  40. // Returns corresponding device type icons for given Bluetooth device types and
  41. // connection states.
  42. const gfx::VectorIcon& GetBluetoothDeviceIcon(
  43. BluetoothDeviceInfo::DeviceType device_type,
  44. BluetoothDeviceInfo::ConnectionState connection_state) {
  45. switch (device_type) {
  46. case BluetoothDeviceInfo::DeviceType::kComputer:
  47. return ash::kSystemMenuComputerLegacyIcon;
  48. case BluetoothDeviceInfo::DeviceType::kPhone:
  49. return ash::kSystemMenuPhoneLegacyIcon;
  50. case BluetoothDeviceInfo::DeviceType::kAudio:
  51. case BluetoothDeviceInfo::DeviceType::kCarAudio:
  52. return ash::kSystemMenuHeadsetLegacyIcon;
  53. case BluetoothDeviceInfo::DeviceType::kVideo:
  54. return ash::kSystemMenuVideocamLegacyIcon;
  55. case BluetoothDeviceInfo::DeviceType::kJoystick:
  56. case BluetoothDeviceInfo::DeviceType::kGamepad:
  57. return ash::kSystemMenuGamepadLegacyIcon;
  58. case BluetoothDeviceInfo::DeviceType::kKeyboard:
  59. case BluetoothDeviceInfo::DeviceType::kKeyboardMouseCombo:
  60. return ash::kSystemMenuKeyboardLegacyIcon;
  61. case BluetoothDeviceInfo::DeviceType::kTablet:
  62. return ash::kSystemMenuTabletLegacyIcon;
  63. case BluetoothDeviceInfo::DeviceType::kMouse:
  64. return ash::kSystemMenuMouseLegacyIcon;
  65. case BluetoothDeviceInfo::DeviceType::kModem:
  66. case BluetoothDeviceInfo::DeviceType::kPeripheral:
  67. return ash::kSystemMenuBluetoothLegacyIcon;
  68. default:
  69. return connection_state ==
  70. BluetoothDeviceInfo::ConnectionState::kConnected
  71. ? ash::kSystemMenuBluetoothConnectedLegacyIcon
  72. : ash::kSystemMenuBluetoothLegacyIcon;
  73. }
  74. }
  75. views::View* CreateDisabledPanel() {
  76. views::View* container = new views::View;
  77. auto box_layout = std::make_unique<views::BoxLayout>(
  78. views::BoxLayout::Orientation::kVertical);
  79. box_layout->set_main_axis_alignment(
  80. views::BoxLayout::MainAxisAlignment::kCenter);
  81. container->SetLayoutManager(std::move(box_layout));
  82. auto* color_provider = AshColorProvider::Get();
  83. auto* image_view =
  84. container->AddChildView(std::make_unique<views::ImageView>());
  85. image_view->SetImage(gfx::CreateVectorIcon(
  86. kSystemMenuBluetoothDisabledIcon,
  87. AshColorProvider::GetDisabledColor(color_provider->GetContentLayerColor(
  88. AshColorProvider::ContentLayerType::kIconColorPrimary))));
  89. image_view->SetVerticalAlignment(views::ImageView::Alignment::kTrailing);
  90. auto* label = container->AddChildView(std::make_unique<views::Label>(
  91. l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISABLED)));
  92. label->SetEnabledColor(
  93. AshColorProvider::GetDisabledColor(color_provider->GetContentLayerColor(
  94. AshColorProvider::ContentLayerType::kTextColorPrimary)));
  95. TrayPopupUtils::SetLabelFontList(
  96. label, TrayPopupUtils::FontStyle::kDetailedViewLabel);
  97. label->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
  98. kDisabledPanelLabelBaselineY - label->GetBaseline(), 0, 0, 0)));
  99. // Make top padding of the icon equal to the height of the label so that the
  100. // icon is vertically aligned to center of the container.
  101. image_view->SetBorder(views::CreateEmptyBorder(
  102. gfx::Insets::TLBR(label->GetPreferredSize().height(), 0, 0, 0)));
  103. return container;
  104. }
  105. void LogUserBluetoothEvent(const BluetoothAddress& device_address) {
  106. ml::UserSettingsEventLogger* logger = ml::UserSettingsEventLogger::Get();
  107. if (logger) {
  108. logger->LogBluetoothUkmEvent(device_address);
  109. }
  110. }
  111. HoverHighlightView* GetScrollListItemForDevice(
  112. const std::unordered_map<HoverHighlightView*, BluetoothAddress>& device_map,
  113. const BluetoothAddress& address) {
  114. for (const auto& view_and_address : device_map) {
  115. if (view_and_address.second == address)
  116. return view_and_address.first;
  117. }
  118. return nullptr;
  119. }
  120. } // namespace
  121. BluetoothDetailedViewLegacy::BluetoothDetailedViewLegacy(
  122. DetailedViewDelegate* delegate,
  123. LoginStatus login)
  124. : TrayDetailedView(delegate), login_(login) {
  125. CreateItems();
  126. device::RecordUiSurfaceDisplayed(
  127. device::BluetoothUiSurface::kBluetoothQuickSettings);
  128. }
  129. BluetoothDetailedViewLegacy::~BluetoothDetailedViewLegacy() = default;
  130. void BluetoothDetailedViewLegacy::ShowLoadingIndicator() {
  131. // Setting a value of -1 gives progress_bar an infinite-loading behavior.
  132. ShowProgress(-1, true);
  133. }
  134. void BluetoothDetailedViewLegacy::HideLoadingIndicator() {
  135. ShowProgress(0, false);
  136. }
  137. void BluetoothDetailedViewLegacy::ShowBluetoothDisabledPanel() {
  138. device_map_.clear();
  139. paired_devices_heading_ = nullptr;
  140. unpaired_devices_heading_ = nullptr;
  141. bluetooth_discovering_label_ = nullptr;
  142. scroll_content()->RemoveAllChildViews();
  143. DCHECK(scroller());
  144. if (!disabled_panel_) {
  145. disabled_panel_ = CreateDisabledPanel();
  146. // Insert |disabled_panel_| before the scroller, since the scroller will
  147. // have unnecessary bottom border when it is not the last child.
  148. AddChildViewAt(disabled_panel_, GetIndexOf(scroller()).value());
  149. // |disabled_panel_| need to fill the remaining space below the title row
  150. // so that the inner contents of |disabled_panel_| are placed properly.
  151. box_layout()->SetFlexForView(disabled_panel_, 1);
  152. }
  153. disabled_panel_->SetVisible(true);
  154. scroller()->SetVisible(false);
  155. Layout();
  156. }
  157. void BluetoothDetailedViewLegacy::HideBluetoothDisabledPanel() {
  158. DCHECK(scroller());
  159. if (disabled_panel_)
  160. disabled_panel_->SetVisible(false);
  161. scroller()->SetVisible(true);
  162. Layout();
  163. }
  164. bool BluetoothDetailedViewLegacy::IsDeviceScrollListEmpty() const {
  165. return device_map_.empty();
  166. }
  167. void BluetoothDetailedViewLegacy::UpdateDeviceScrollList(
  168. const BluetoothDeviceList& connected_devices,
  169. const BluetoothDeviceList& connecting_devices,
  170. const BluetoothDeviceList& paired_not_connected_devices,
  171. const BluetoothDeviceList& discovered_not_paired_devices) {
  172. connecting_devices_.clear();
  173. for (const auto& device : connecting_devices)
  174. connecting_devices_.push_back(device->Clone());
  175. paired_not_connected_devices_.clear();
  176. for (const auto& device : paired_not_connected_devices)
  177. paired_not_connected_devices_.push_back(device->Clone());
  178. // Keep track of previous device_map_ so that existing scroll list
  179. // item views can be re-used. This is required for a11y so that
  180. // keyboard focus and screen-reader call outs are not disrupted
  181. // by frequent device list updates.
  182. std::unordered_map<HoverHighlightView*, BluetoothAddress> old_device_map =
  183. device_map_;
  184. device_map_.clear();
  185. // Add paired devices and their section header to the list.
  186. bool has_paired_devices = !connected_devices.empty() ||
  187. !connecting_devices.empty() ||
  188. !paired_not_connected_devices.empty();
  189. size_t index = 0;
  190. if (has_paired_devices) {
  191. paired_devices_heading_ =
  192. AddSubHeading(IDS_ASH_STATUS_TRAY_BLUETOOTH_PAIRED_DEVICES,
  193. paired_devices_heading_, index++);
  194. index = AddSameTypeDevicesToScrollList(connected_devices, old_device_map,
  195. index, true, true);
  196. index = AddSameTypeDevicesToScrollList(connecting_devices, old_device_map,
  197. index, true, false);
  198. index = AddSameTypeDevicesToScrollList(paired_not_connected_devices,
  199. old_device_map, index, false, false);
  200. } else if (paired_devices_heading_) {
  201. scroll_content()->RemoveChildViewT(paired_devices_heading_);
  202. paired_devices_heading_ = nullptr;
  203. }
  204. // Add unpaired devices to the list. If at least one paired device is
  205. // present, also add a section header above the unpaired devices.
  206. if (!discovered_not_paired_devices.empty()) {
  207. if (has_paired_devices) {
  208. unpaired_devices_heading_ =
  209. AddSubHeading(IDS_ASH_STATUS_TRAY_BLUETOOTH_UNPAIRED_DEVICES,
  210. unpaired_devices_heading_, index++);
  211. }
  212. index = AddSameTypeDevicesToScrollList(discovered_not_paired_devices,
  213. old_device_map, index, false, false);
  214. }
  215. if (unpaired_devices_heading_ &&
  216. (discovered_not_paired_devices.empty() || !has_paired_devices)) {
  217. scroll_content()->RemoveChildViewT(unpaired_devices_heading_);
  218. unpaired_devices_heading_ = nullptr;
  219. }
  220. // Show user Bluetooth state if there is no bluetooth devices in list.
  221. if (device_map_.empty()) {
  222. if (!bluetooth_discovering_label_) {
  223. bluetooth_discovering_label_ =
  224. new TrayInfoLabel(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERING);
  225. scroll_content()->AddChildViewAt(bluetooth_discovering_label_, index++);
  226. } else {
  227. scroll_content()->ReorderChildView(bluetooth_discovering_label_, index++);
  228. }
  229. } else if (bluetooth_discovering_label_) {
  230. scroll_content()->RemoveChildViewT(bluetooth_discovering_label_);
  231. bluetooth_discovering_label_ = nullptr;
  232. }
  233. // Remove views for devices from old_device_map that are not in device_map_.
  234. for (auto& view_and_address : old_device_map) {
  235. if (device_map_.find(view_and_address.first) == device_map_.end()) {
  236. scroll_content()->RemoveChildViewT(view_and_address.first);
  237. }
  238. }
  239. scroll_content()->InvalidateLayout();
  240. Layout();
  241. }
  242. void BluetoothDetailedViewLegacy::SetToggleIsOn(bool is_on) {
  243. if (toggle_)
  244. toggle_->AnimateIsOn(is_on);
  245. }
  246. const char* BluetoothDetailedViewLegacy::GetClassName() const {
  247. return "BluetoothDetailedViewLegacy";
  248. }
  249. void BluetoothDetailedViewLegacy::CreateItems() {
  250. CreateScrollableList();
  251. scroll_content()->SetID(kScrollContentID);
  252. CreateTitleRow(IDS_ASH_STATUS_TRAY_BLUETOOTH);
  253. }
  254. TriView* BluetoothDetailedViewLegacy::AddSubHeading(int text_id,
  255. TriView* sub_heading_view,
  256. size_t child_index) {
  257. if (!sub_heading_view) {
  258. sub_heading_view = AddScrollListSubHeader(text_id);
  259. }
  260. scroll_content()->ReorderChildView(sub_heading_view, child_index);
  261. return sub_heading_view;
  262. }
  263. size_t BluetoothDetailedViewLegacy::AddSameTypeDevicesToScrollList(
  264. const BluetoothDeviceList& list,
  265. const std::unordered_map<HoverHighlightView*, BluetoothAddress>&
  266. old_device_list,
  267. size_t child_index,
  268. bool highlight,
  269. bool checked) {
  270. for (const auto& device : list) {
  271. const gfx::VectorIcon& icon =
  272. GetBluetoothDeviceIcon(device->device_type, device->connection_state);
  273. std::u16string device_name =
  274. device::GetBluetoothDeviceNameForDisplay(device);
  275. HoverHighlightView* container =
  276. GetScrollListItemForDevice(old_device_list, device->address);
  277. if (!container) {
  278. container = AddScrollListItem(icon, device_name);
  279. } else {
  280. container->text_label()->SetText(device_name);
  281. DCHECK(views::IsViewClass<views::ImageView>(container->left_view()));
  282. views::ImageView* left_icon =
  283. static_cast<views::ImageView*>(container->left_view());
  284. left_icon->SetImage(gfx::CreateVectorIcon(
  285. icon, AshColorProvider::Get()->GetContentLayerColor(
  286. AshColorProvider::ContentLayerType::kIconColorPrimary)));
  287. }
  288. if (device->is_blocked_by_policy) {
  289. if (container->right_view()) {
  290. container->SetRightViewVisible(true);
  291. } else {
  292. gfx::ImageSkia enterprise_managed_icon = CreateVectorIcon(
  293. chromeos::kEnterpriseIcon, kEnterpriseManagedIconSizeDip,
  294. gfx::kGoogleGrey100);
  295. container->AddRightIcon(enterprise_managed_icon,
  296. enterprise_managed_icon.width());
  297. }
  298. } else if (container->right_view()) {
  299. container->SetRightViewVisible(false);
  300. }
  301. container->SetAccessibleName(
  302. device::GetBluetoothDeviceLabelForAccessibility(device));
  303. switch (device->connection_state) {
  304. case BluetoothDeviceInfo::ConnectionState::kNotConnected:
  305. break;
  306. case BluetoothDeviceInfo::ConnectionState::kConnecting:
  307. SetupConnectingScrollListItem(container);
  308. break;
  309. case BluetoothDeviceInfo::ConnectionState::kConnected:
  310. SetupConnectedScrollListItem(
  311. container, device->battery_info
  312. ? absl::make_optional<uint8_t>(
  313. device->battery_info->battery_percentage)
  314. : absl::nullopt);
  315. break;
  316. }
  317. scroll_content()->ReorderChildView(container, child_index++);
  318. device_map_[container] = device->address;
  319. }
  320. return child_index;
  321. }
  322. bool BluetoothDetailedViewLegacy::FoundDevice(
  323. const BluetoothAddress& device_address,
  324. const BluetoothDeviceList& device_list) const {
  325. for (const auto& device : device_list) {
  326. if (device->address == device_address)
  327. return true;
  328. }
  329. return false;
  330. }
  331. void BluetoothDetailedViewLegacy::UpdateClickedDevice(
  332. const BluetoothAddress& device_address,
  333. HoverHighlightView* item_container) {
  334. if (FoundDevice(device_address, paired_not_connected_devices_)) {
  335. SetupConnectingScrollListItem(item_container);
  336. scroll_content()->SizeToPreferredSize();
  337. scroller()->Layout();
  338. }
  339. }
  340. void BluetoothDetailedViewLegacy::ToggleButtonPressed() {
  341. Shell::Get()->tray_bluetooth_helper()->SetBluetoothEnabled(
  342. toggle_->GetIsOn());
  343. }
  344. void BluetoothDetailedViewLegacy::ShowSettings() {
  345. if (TrayPopupUtils::CanOpenWebUISettings()) {
  346. CloseBubble(); // Deletes |this|.
  347. Shell::Get()->system_tray_model()->client()->ShowBluetoothSettings();
  348. }
  349. }
  350. absl::optional<BluetoothAddress>
  351. BluetoothDetailedViewLegacy::GetFocusedDeviceAddress() const {
  352. for (const auto& view_and_address : device_map_) {
  353. if (view_and_address.first->HasFocus())
  354. return view_and_address.second;
  355. }
  356. return absl::nullopt;
  357. }
  358. void BluetoothDetailedViewLegacy::FocusDeviceByAddress(
  359. const BluetoothAddress& address) const {
  360. for (auto& view_and_address : device_map_) {
  361. if (view_and_address.second == address) {
  362. view_and_address.first->RequestFocus();
  363. return;
  364. }
  365. }
  366. }
  367. void BluetoothDetailedViewLegacy::HandleViewClicked(views::View* view) {
  368. TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper();
  369. if (helper->GetBluetoothState() != BluetoothSystem::State::kPoweredOn)
  370. return;
  371. HoverHighlightView* container = static_cast<HoverHighlightView*>(view);
  372. std::unordered_map<HoverHighlightView*, BluetoothAddress>::iterator find;
  373. find = device_map_.find(container);
  374. if (find == device_map_.end())
  375. return;
  376. const BluetoothAddress& device_address = find->second;
  377. if (FoundDevice(device_address, connecting_devices_))
  378. return;
  379. UpdateClickedDevice(device_address, container);
  380. LogUserBluetoothEvent(device_address);
  381. helper->ConnectToBluetoothDevice(device_address);
  382. }
  383. void BluetoothDetailedViewLegacy::CreateExtraTitleRowButtons() {
  384. if (login_ == LoginStatus::LOCKED)
  385. return;
  386. DCHECK(!toggle_);
  387. DCHECK(!settings_);
  388. tri_view()->SetContainerVisible(TriView::Container::END, true);
  389. toggle_ = new TrayToggleButton(
  390. base::BindRepeating(&BluetoothDetailedViewLegacy::ToggleButtonPressed,
  391. base::Unretained(this)),
  392. IDS_ASH_STATUS_TRAY_BLUETOOTH);
  393. toggle_->SetIsOn(Shell::Get()->tray_bluetooth_helper()->GetBluetoothState() ==
  394. BluetoothSystem::State::kPoweredOn);
  395. tri_view()->AddView(TriView::Container::END, toggle_);
  396. settings_ = CreateSettingsButton(
  397. base::BindRepeating(&BluetoothDetailedViewLegacy::ShowSettings,
  398. base::Unretained(this)),
  399. IDS_ASH_STATUS_TRAY_BLUETOOTH_SETTINGS);
  400. tri_view()->AddView(TriView::Container::END, settings_);
  401. }
  402. } // namespace ash