chromium/ash/system/bluetooth/bluetooth_detailed_view_controller.cc

// Copyright 2021 The Chromium Authors
// 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_controller.h"

#include <optional>

#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/bluetooth_config_service.h"
#include "ash/public/cpp/hats_bluetooth_revamp_trigger.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/bluetooth/hid_preserving_controller/hid_preserving_bluetooth_state_service.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/memory/ptr_util.h"
#include "build/chromeos_buildflags.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/services/bluetooth_config/public/cpp/cros_bluetooth_config_util.h"
#include "chromeos/constants/chromeos_features.h"
#include "mojo/public/cpp/bindings/clone_traits.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/view.h"

namespace ash {

using bluetooth_config::IsBluetoothEnabledOrEnabling;
using bluetooth_config::mojom::AudioOutputCapability;
using bluetooth_config::mojom::BatteryProperties;
using bluetooth_config::mojom::BluetoothDeviceProperties;
using bluetooth_config::mojom::BluetoothDevicePropertiesPtr;
using bluetooth_config::mojom::BluetoothSystemState;
using bluetooth_config::mojom::DeviceBatteryInfo;
using bluetooth_config::mojom::DeviceBatteryInfoPtr;
using bluetooth_config::mojom::DeviceConnectionState;
using bluetooth_config::mojom::DeviceType;
using bluetooth_config::mojom::PairedBluetoothDeviceProperties;
using bluetooth_config::mojom::PairedBluetoothDevicePropertiesPtr;

BluetoothDetailedViewController::BluetoothDetailedViewController(
    UnifiedSystemTrayController* tray_controller)
    : detailed_view_delegate_(
          std::make_unique<DetailedViewDelegate>(tray_controller)),
      tray_controller_(tray_controller) {
  GetBluetoothConfigService(
      remote_cros_bluetooth_config_.BindNewPipeAndPassReceiver());
  remote_cros_bluetooth_config_->ObserveSystemProperties(
      cros_system_properties_observer_receiver_.BindNewPipeAndPassRemote());

  if (features::IsBluetoothDisconnectWarningEnabled()) {
    GetHidPreservingBluetoothStateControllerService(
        remote_hid_preserving_bluetooth_.BindNewPipeAndPassReceiver());
  }
}

BluetoothDetailedViewController::~BluetoothDetailedViewController() = default;

std::unique_ptr<views::View> BluetoothDetailedViewController::CreateView() {
  DCHECK(!view_);
  std::unique_ptr<BluetoothDetailedView> bluetooth_detailed_view =
      BluetoothDetailedView::Factory::Create(detailed_view_delegate_.get(),
                                             /*delegate=*/this);
  view_ = bluetooth_detailed_view.get();
  device_list_controller_ = BluetoothDeviceListController::Factory::Create(
      bluetooth_detailed_view.get());
  BluetoothEnabledStateChanged();

  if (IsBluetoothEnabledOrEnabling(system_state_)) {
    device_list_controller_->UpdateDeviceList(connected_devices_,
                                              previously_connected_devices_);
  }

  // `bluetooth_detailed_view` is not a views::View, so we must GetAsView().
  return base::WrapUnique(bluetooth_detailed_view.release()->GetAsView());
}

std::u16string BluetoothDetailedViewController::GetAccessibleName() const {
  return l10n_util::GetStringUTF16(
      IDS_ASH_QUICK_SETTINGS_BUBBLE_BLUETOOTH_SETTINGS_ACCESSIBLE_DESCRIPTION);
}

void BluetoothDetailedViewController::OnPropertiesUpdated(
    bluetooth_config::mojom::BluetoothSystemPropertiesPtr properties) {
  // The tray controller should only be transitioning to the main view when this
  // feature is disabled since the detailed tray view and the Bluetooth Pod in
  // QS would be hidden. However, when the feature is enabled, the Bluetooth Pod
  // is visible and the user should be able to see the detailed tray view.
  if (!chromeos::features::IsBluetoothWifiQSPodRefreshEnabled() &&
      properties->system_state == BluetoothSystemState::kUnavailable) {
    tray_controller_->TransitionToMainView(
        /*restore_focus=*/true);  // Deletes |this|.
    return;
  }

  const bool has_bluetooth_enabled_state_changed =
      system_state_ != properties->system_state;
  system_state_ = properties->system_state;

  if (has_bluetooth_enabled_state_changed)
    BluetoothEnabledStateChanged();

  connected_devices_.clear();
  previously_connected_devices_.clear();

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kQsAddFakeBluetoothDevices)) {
    AddFakeBluetoothDevices();
  }

  for (auto& paired_device : properties->paired_devices) {
    if (paired_device->device_properties->connection_state ==
        DeviceConnectionState::kConnected) {
      connected_devices_.push_back(std::move(paired_device));
    } else {
      previously_connected_devices_.push_back(std::move(paired_device));
    }
  }
  if (device_list_controller_ && IsBluetoothEnabledOrEnabling(system_state_)) {
    device_list_controller_->UpdateDeviceList(connected_devices_,
                                              previously_connected_devices_);
  }
}

void BluetoothDetailedViewController::OnToggleClicked(bool new_state) {
  if (features::IsBluetoothDisconnectWarningEnabled()) {
    remote_hid_preserving_bluetooth_->TryToSetBluetoothEnabledState(
        new_state, mojom::HidWarningDialogSource::kQuickSettings);
  } else {
    remote_cros_bluetooth_config_->SetBluetoothEnabledState(new_state);
  }

  if (auto* hats_bluetooth_revamp_trigger = HatsBluetoothRevampTrigger::Get()) {
    hats_bluetooth_revamp_trigger->TryToShowSurvey();
  }
}

void BluetoothDetailedViewController::OnPairNewDeviceRequested() {
  tray_controller_->CloseBubble();  // Deletes |this|.
  NET_LOG(EVENT) << "Attempting to show the bluetooth pairing dialog";
  Shell::Get()->system_tray_model()->client()->ShowBluetoothPairingDialog(
      /*device_address=*/std::nullopt);

  if (auto* hats_bluetooth_revamp_trigger = HatsBluetoothRevampTrigger::Get()) {
    hats_bluetooth_revamp_trigger->TryToShowSurvey();
  }
}

void BluetoothDetailedViewController::OnDeviceListItemSelected(
    const bluetooth_config::mojom::PairedBluetoothDevicePropertiesPtr& device) {
  // When CloseBubble() is called |device| will be deleted so we need to make a
  // copy of the device ID that was selected.
  const std::string device_id = device->device_properties->id;

  // Non-HID devices can be explicitly connected to, so we detect when this is
  // the case and attempt to connect to the device instead of navigating to the
  // Bluetooth Settings.
  if (device->device_properties->audio_capability ==
          AudioOutputCapability::kCapableOfAudioOutput &&
      device->device_properties->connection_state ==
          DeviceConnectionState::kNotConnected) {
    remote_cros_bluetooth_config_->Connect(device_id,
                                           /*callback=*/base::DoNothing());
    return;
  }
  tray_controller_->CloseBubble();  // Deletes |this|.
  Shell::Get()->system_tray_model()->client()->ShowBluetoothSettings(device_id);
}

void BluetoothDetailedViewController::BluetoothEnabledStateChanged() {
  if (view_)
    view_->UpdateBluetoothEnabledState(system_state_);
  if (device_list_controller_) {
    device_list_controller_->UpdateBluetoothEnabledState(
        IsBluetoothEnabledOrEnabling(system_state_));
  }
}

void BluetoothDetailedViewController::AddFakeBluetoothDevices() {
#if !BUILDFLAG(IS_CHROMEOS_DEVICE)
  // Only add fake devices in the linux-chromeos emulator. We don't want to
  // increase the binary size for real devices.
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Unknown";
    paired_device_properties->device_properties->public_name = u"Unknown";
    paired_device_properties->device_properties->connection_state =
        DeviceConnectionState::kConnected;
    paired_device_properties->device_properties->device_type =
        DeviceType::kUnknown;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Computer";
    paired_device_properties->device_properties->public_name = u"Computer";
    paired_device_properties->device_properties->device_type =
        DeviceType::kComputer;
    paired_device_properties->device_properties->connection_state =
        DeviceConnectionState::kConnected;
    paired_device_properties->device_properties->battery_info =
        DeviceBatteryInfo::New();
    paired_device_properties->device_properties->battery_info
        ->default_properties = BatteryProperties::New();
    paired_device_properties->device_properties->battery_info
        ->default_properties->battery_percentage = 75;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Phone";
    paired_device_properties->device_properties->public_name = u"Phone";
    paired_device_properties->device_properties->device_type =
        DeviceType::kPhone;
    paired_device_properties->device_properties->connection_state =
        DeviceConnectionState::kConnected;
    paired_device_properties->device_properties->battery_info =
        DeviceBatteryInfo::New();
    paired_device_properties->device_properties->battery_info->left_bud_info =
        BatteryProperties::New();
    paired_device_properties->device_properties->battery_info->left_bud_info
        ->battery_percentage = 5;
    paired_device_properties->device_properties->battery_info->case_info =
        BatteryProperties::New();
    paired_device_properties->device_properties->battery_info->case_info
        ->battery_percentage = 64;
    paired_device_properties->device_properties->battery_info->right_bud_info =
        BatteryProperties::New();
    paired_device_properties->device_properties->battery_info->right_bud_info
        ->battery_percentage = 9;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Game Controller";
    paired_device_properties->device_properties->public_name =
        u"Game Controller";
    paired_device_properties->device_properties->device_type =
        DeviceType::kGameController;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Keyboard";
    paired_device_properties->device_properties->public_name = u"Keyboard";
    paired_device_properties->device_properties->device_type =
        DeviceType::kKeyboard;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Mouse";
    paired_device_properties->device_properties->public_name = u"Mouse";
    paired_device_properties->device_properties->device_type =
        DeviceType::kMouse;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Tablet";
    paired_device_properties->device_properties->public_name = u"Tablet";
    paired_device_properties->device_properties->device_type =
        DeviceType::kTablet;

    connected_devices_.push_back(mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Headset";
    paired_device_properties->device_properties->public_name = u"Headset";
    paired_device_properties->device_properties->device_type =
        DeviceType::kHeadset;
    paired_device_properties->device_properties->connection_state =
        DeviceConnectionState::kConnecting;

    previously_connected_devices_.push_back(
        mojo::Clone(paired_device_properties));
  }
  {
    PairedBluetoothDevicePropertiesPtr paired_device_properties =
        PairedBluetoothDeviceProperties::New();
    paired_device_properties->device_properties =
        BluetoothDeviceProperties::New();
    paired_device_properties->device_properties->id = "Video Camera";
    paired_device_properties->device_properties->public_name =
        u"Video Camera with a very very very very very very very long name";
    paired_device_properties->device_properties->device_type =
        DeviceType::kVideoCamera;
    paired_device_properties->device_properties->connection_state =
        DeviceConnectionState::kNotConnected;

    previously_connected_devices_.push_back(
        mojo::Clone(paired_device_properties));
  }
#endif  // !BUILDFLAG(IS_CHROMEOS_DEVICE)
}

void BluetoothDetailedViewController::ShutDown() {
  device_list_controller_.reset();
}

}  // namespace ash