// 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 <memory>
#include <optional>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/fake_hats_bluetooth_revamp_trigger_impl.h"
#include "ash/public/cpp/hats_bluetooth_revamp_trigger.h"
#include "ash/public/cpp/test/test_system_tray_client.h"
#include "ash/system/bluetooth/bluetooth_detailed_view.h"
#include "ash/system/bluetooth/bluetooth_device_list_controller.h"
#include "ash/system/bluetooth/fake_bluetooth_detailed_view.h"
#include "ash/system/bluetooth/fake_bluetooth_device_list_controller.h"
#include "ash/system/bluetooth/hid_preserving_controller/hid_preserving_bluetooth_state_controller_test_helper.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_helper.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/services/bluetooth_config/fake_adapter_state_controller.h"
#include "chromeos/ash/services/bluetooth_config/fake_device_cache.h"
#include "chromeos/ash/services/bluetooth_config/fake_device_operation_handler.h"
#include "chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom.h"
#include "chromeos/ash/services/bluetooth_config/scoped_bluetooth_config_test_helper.h"
#include "mojo/public/cpp/bindings/clone_traits.h"
namespace ash {
namespace {
using bluetooth_config::FakeDeviceOperationHandler;
using bluetooth_config::ScopedBluetoothConfigTestHelper;
using bluetooth_config::mojom::AudioOutputCapability;
using bluetooth_config::mojom::BluetoothDeviceProperties;
using bluetooth_config::mojom::BluetoothSystemState;
using bluetooth_config::mojom::DeviceConnectionState;
using bluetooth_config::mojom::PairedBluetoothDeviceProperties;
using bluetooth_config::mojom::PairedBluetoothDevicePropertiesPtr;
const char kDeviceId[] = "/device/id";
class FakeBluetoothDetailedViewFactory : public BluetoothDetailedView::Factory {
public:
FakeBluetoothDetailedViewFactory() = default;
FakeBluetoothDetailedViewFactory(const FakeBluetoothDetailedViewFactory&) =
delete;
const FakeBluetoothDetailedViewFactory& operator=(
const FakeBluetoothDetailedViewFactory&) = delete;
~FakeBluetoothDetailedViewFactory() override = default;
FakeBluetoothDetailedView* bluetooth_detailed_view() {
return bluetooth_detailed_view_;
}
private:
std::unique_ptr<BluetoothDetailedView> CreateForTesting(
BluetoothDetailedView::Delegate* delegate) override {
DCHECK(!bluetooth_detailed_view_);
std::unique_ptr<FakeBluetoothDetailedView> bluetooth_detailed_view =
std::make_unique<FakeBluetoothDetailedView>(delegate);
bluetooth_detailed_view_ = bluetooth_detailed_view.get();
return bluetooth_detailed_view;
}
raw_ptr<FakeBluetoothDetailedView, DanglingUntriaged>
bluetooth_detailed_view_ = nullptr;
};
class FakeBluetoothDeviceListControllerFactory
: public BluetoothDeviceListController::Factory {
public:
FakeBluetoothDeviceListControllerFactory() = default;
FakeBluetoothDeviceListControllerFactory(
const FakeBluetoothDeviceListControllerFactory&) = delete;
const FakeBluetoothDeviceListControllerFactory& operator=(
const FakeBluetoothDeviceListControllerFactory&) = delete;
~FakeBluetoothDeviceListControllerFactory() override = default;
FakeBluetoothDeviceListController* bluetooth_device_list_controller() {
return bluetooth_device_list_controller_;
}
private:
std::unique_ptr<BluetoothDeviceListController> CreateForTesting() override {
DCHECK(!bluetooth_device_list_controller_);
std::unique_ptr<FakeBluetoothDeviceListController>
bluetooth_device_list_controller =
std::make_unique<FakeBluetoothDeviceListController>();
bluetooth_device_list_controller_ = bluetooth_device_list_controller.get();
return bluetooth_device_list_controller;
}
raw_ptr<FakeBluetoothDeviceListController, DanglingUntriaged>
bluetooth_device_list_controller_ = nullptr;
};
} // namespace
class BluetoothDetailedViewControllerTest : public AshTestBase {
public:
void SetUp() override {
AshTestBase::SetUp();
GetPrimaryUnifiedSystemTray()->ShowBubble();
BluetoothDetailedView::Factory::SetFactoryForTesting(
&bluetooth_detailed_view_factory_);
BluetoothDeviceListController::Factory::SetFactoryForTesting(
&bluetooth_device_list_controller_factory_);
GetPrimaryUnifiedSystemTray()
->bubble()
->unified_system_tray_controller()
->ShowBluetoothDetailedView();
fake_trigger_impl_ = std::make_unique<FakeHatsBluetoothRevampTriggerImpl>();
bluetooth_detailed_view_controller_ =
static_cast<BluetoothDetailedViewController*>(
GetPrimaryUnifiedSystemTray()
->bubble()
->unified_system_tray_controller()
->detailed_view_controller());
base::RunLoop().RunUntilIdle();
}
void TearDown() override {
BluetoothDeviceListController::Factory::SetFactoryForTesting(nullptr);
BluetoothDetailedView::Factory::SetFactoryForTesting(nullptr);
AshTestBase::TearDown();
}
BluetoothSystemState GetBluetoothAdapterState() {
return bluetooth_config_test_helper()
->fake_adapter_state_controller()
->GetAdapterState();
}
PairedBluetoothDevicePropertiesPtr CreatePairedDevice(
DeviceConnectionState connection_state) {
PairedBluetoothDevicePropertiesPtr paired_properties =
PairedBluetoothDeviceProperties::New();
paired_properties->device_properties = BluetoothDeviceProperties::New();
paired_properties->device_properties->connection_state = connection_state;
return paired_properties;
}
void SetPairedDevices(
std::vector<PairedBluetoothDevicePropertiesPtr> paired_devices) {
bluetooth_config_test_helper()->fake_device_cache()->SetPairedDevices(
std::move(paired_devices));
base::RunLoop().RunUntilIdle();
}
void SetBluetoothAdapterState(BluetoothSystemState system_state) {
bluetooth_config_test_helper()
->fake_adapter_state_controller()
->SetSystemState(system_state);
base::RunLoop().RunUntilIdle();
}
BluetoothDetailedView::Delegate* bluetooth_detailed_view_delegate() {
return bluetooth_detailed_view_controller_;
}
FakeBluetoothDetailedView* bluetooth_detailed_view() {
return bluetooth_detailed_view_factory_.bluetooth_detailed_view();
}
FakeBluetoothDeviceListController* bluetooth_device_list_controller() {
return bluetooth_device_list_controller_factory_
.bluetooth_device_list_controller();
}
FakeDeviceOperationHandler* fake_device_operation_handler() {
return bluetooth_config_test_helper()->fake_device_operation_handler();
}
size_t GetTryToShowSurveyCount() {
return fake_trigger_impl_->try_to_show_survey_count();
}
private:
ScopedBluetoothConfigTestHelper* bluetooth_config_test_helper() {
return ash_test_helper()->bluetooth_config_test_helper();
}
std::unique_ptr<FakeHatsBluetoothRevampTriggerImpl> fake_trigger_impl_;
raw_ptr<BluetoothDetailedViewController, DanglingUntriaged>
bluetooth_detailed_view_controller_;
FakeBluetoothDetailedViewFactory bluetooth_detailed_view_factory_;
FakeBluetoothDeviceListControllerFactory
bluetooth_device_list_controller_factory_;
};
TEST_F(BluetoothDetailedViewControllerTest,
TransitionToMainViewWhenBluetoothUnavailable) {
EXPECT_TRUE(GetPrimaryUnifiedSystemTray()
->bubble()
->unified_system_tray_controller()
->IsDetailedViewShown());
SetBluetoothAdapterState(BluetoothSystemState::kUnavailable);
EXPECT_FALSE(GetPrimaryUnifiedSystemTray()
->bubble()
->unified_system_tray_controller()
->IsDetailedViewShown());
}
TEST_F(BluetoothDetailedViewControllerTest,
NotifiesWhenBluetoothEnabledStateChanges) {
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_TRUE(
bluetooth_detailed_view()->last_bluetooth_enabled_state().has_value());
EXPECT_TRUE(
bluetooth_detailed_view()->last_bluetooth_enabled_state().value());
EXPECT_TRUE(bluetooth_device_list_controller()
->last_bluetooth_enabled_state()
.has_value());
EXPECT_TRUE(bluetooth_device_list_controller()
->last_bluetooth_enabled_state()
.value());
SetBluetoothAdapterState(BluetoothSystemState::kDisabled);
EXPECT_FALSE(
bluetooth_detailed_view()->last_bluetooth_enabled_state().value());
EXPECT_FALSE(bluetooth_device_list_controller()
->last_bluetooth_enabled_state()
.value());
SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
EXPECT_TRUE(
bluetooth_detailed_view()->last_bluetooth_enabled_state().value());
EXPECT_TRUE(bluetooth_device_list_controller()
->last_bluetooth_enabled_state()
.value());
}
TEST_F(BluetoothDetailedViewControllerTest,
ChangesBluetoothEnabledStateWhenTogglePressed) {
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(0u, GetTryToShowSurveyCount());
bluetooth_detailed_view_delegate()->OnToggleClicked(/*new_state=*/false);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
bluetooth_detailed_view_delegate()->OnToggleClicked(/*new_state=*/true);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kEnabling, GetBluetoothAdapterState());
EXPECT_EQ(2u, GetTryToShowSurveyCount());
}
TEST_F(BluetoothDetailedViewControllerTest,
OnPairNewDeviceRequestedOpensBluetoothDialogWithHatsTrigger) {
EXPECT_EQ(0u, GetTryToShowSurveyCount());
EXPECT_EQ(0, GetSystemTrayClient()->show_bluetooth_pairing_dialog_count());
bluetooth_detailed_view_delegate()->OnPairNewDeviceRequested();
EXPECT_EQ(1, GetSystemTrayClient()->show_bluetooth_pairing_dialog_count());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
}
TEST_F(BluetoothDetailedViewControllerTest,
OnDeviceListAudioCapableItemSelected) {
PairedBluetoothDevicePropertiesPtr selected_device =
CreatePairedDevice(DeviceConnectionState::kNotConnected);
selected_device->device_properties->id = kDeviceId;
selected_device->device_properties->audio_capability =
AudioOutputCapability::kCapableOfAudioOutput;
std::vector<PairedBluetoothDevicePropertiesPtr> paired_devices;
paired_devices.push_back(mojo::Clone(selected_device));
SetPairedDevices(std::move(paired_devices));
EXPECT_EQ(0, GetSystemTrayClient()->show_bluetooth_settings_count());
EXPECT_TRUE(
GetSystemTrayClient()->last_bluetooth_settings_device_id().empty());
EXPECT_EQ(0u, fake_device_operation_handler()->perform_connect_call_count());
bluetooth_detailed_view_delegate()->OnDeviceListItemSelected(selected_device);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(0, GetSystemTrayClient()->show_bluetooth_settings_count());
EXPECT_TRUE(
GetSystemTrayClient()->last_bluetooth_settings_device_id().empty());
EXPECT_EQ(1u, fake_device_operation_handler()->perform_connect_call_count());
EXPECT_STREQ(kDeviceId, fake_device_operation_handler()
->last_perform_connect_device_id()
.c_str());
selected_device->device_properties->connection_state =
DeviceConnectionState::kConnected;
bluetooth_detailed_view_delegate()->OnDeviceListItemSelected(selected_device);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(1, GetSystemTrayClient()->show_bluetooth_settings_count());
EXPECT_STREQ(
kDeviceId,
GetSystemTrayClient()->last_bluetooth_settings_device_id().c_str());
}
TEST_F(BluetoothDetailedViewControllerTest,
OnDeviceListNonAudioCapableItemSelected) {
PairedBluetoothDevicePropertiesPtr selected_device =
CreatePairedDevice(DeviceConnectionState::kNotConnected);
selected_device->device_properties->id = kDeviceId;
EXPECT_EQ(0, GetSystemTrayClient()->show_bluetooth_settings_count());
EXPECT_TRUE(
GetSystemTrayClient()->last_bluetooth_settings_device_id().empty());
bluetooth_detailed_view_delegate()->OnDeviceListItemSelected(selected_device);
EXPECT_EQ(1, GetSystemTrayClient()->show_bluetooth_settings_count());
EXPECT_STREQ(
kDeviceId,
GetSystemTrayClient()->last_bluetooth_settings_device_id().c_str());
EXPECT_EQ(0u, fake_device_operation_handler()->perform_connect_call_count());
}
TEST_F(BluetoothDetailedViewControllerTest,
CorrectlySplitsDevicesByConnectionState) {
std::vector<PairedBluetoothDevicePropertiesPtr> paired_devices;
paired_devices.push_back(
CreatePairedDevice(DeviceConnectionState::kNotConnected));
paired_devices.push_back(
CreatePairedDevice(DeviceConnectionState::kConnecting));
paired_devices.push_back(
CreatePairedDevice(DeviceConnectionState::kConnected));
EXPECT_EQ(0u, bluetooth_device_list_controller()->connected_devices_count());
EXPECT_EQ(
0u,
bluetooth_device_list_controller()->previously_connected_devices_count());
SetPairedDevices(std::move(paired_devices));
EXPECT_EQ(1u, bluetooth_device_list_controller()->connected_devices_count());
EXPECT_EQ(
2u,
bluetooth_device_list_controller()->previously_connected_devices_count());
}
class BluetoothDetailedViewControllerConnectWarningTest : public AshTestBase {
public:
void SetUp() override {
AshTestBase::SetUp();
scoped_feature_list_.InitAndEnableFeature(
features::kBluetoothDisconnectWarning);
GetPrimaryUnifiedSystemTray()->ShowBubble();
hid_preserving_bluetooth_state_test_helper_ =
std::make_unique<HidPreservingBluetoothStateControllerTestHelper>();
hid_preserving_bluetooth_state_test_helper_->fake_hid_preserving_bluetooth()
->SetScopedBluetoothConfigHelper(bluetooth_config_test_helper());
BluetoothDetailedView::Factory::SetFactoryForTesting(
&bluetooth_detailed_view_factory_);
BluetoothDeviceListController::Factory::SetFactoryForTesting(
&bluetooth_device_list_controller_factory_);
GetPrimaryUnifiedSystemTray()
->bubble()
->unified_system_tray_controller()
->ShowBluetoothDetailedView();
fake_trigger_impl_ = std::make_unique<FakeHatsBluetoothRevampTriggerImpl>();
bluetooth_detailed_view_controller_ =
static_cast<BluetoothDetailedViewController*>(
GetPrimaryUnifiedSystemTray()
->bubble()
->unified_system_tray_controller()
->detailed_view_controller());
base::RunLoop().RunUntilIdle();
}
void TearDown() override {
BluetoothDeviceListController::Factory::SetFactoryForTesting(nullptr);
BluetoothDetailedView::Factory::SetFactoryForTesting(nullptr);
AshTestBase::TearDown();
}
BluetoothSystemState GetBluetoothAdapterState() {
return bluetooth_config_test_helper()
->fake_adapter_state_controller()
->GetAdapterState();
}
void SetBluetoothAdapterState(BluetoothSystemState system_state) {
bluetooth_config_test_helper()
->fake_adapter_state_controller()
->SetSystemState(system_state);
base::RunLoop().RunUntilIdle();
}
BluetoothDetailedView::Delegate* bluetooth_detailed_view_delegate() {
return bluetooth_detailed_view_controller_;
}
size_t GetTryToShowSurveyCount() {
return fake_trigger_impl_->try_to_show_survey_count();
}
void SetShouldShowWarningDialog(bool should_show_warning_dialog) {
hid_preserving_bluetooth_state_test_helper_->fake_hid_preserving_bluetooth()
->SetShouldShowWarningDialog(should_show_warning_dialog);
}
void CompleteShowWarningDialog(bool show_dialog_result) {
hid_preserving_bluetooth_state_test_helper_->fake_hid_preserving_bluetooth()
->CompleteShowDialog(show_dialog_result);
}
size_t GetDialogShownCount() {
return hid_preserving_bluetooth_state_test_helper_
->fake_hid_preserving_bluetooth()
->dialog_shown_count();
}
private:
ScopedBluetoothConfigTestHelper* bluetooth_config_test_helper() {
return ash_test_helper()->bluetooth_config_test_helper();
}
std::unique_ptr<FakeHatsBluetoothRevampTriggerImpl> fake_trigger_impl_;
raw_ptr<BluetoothDetailedViewController, DanglingUntriaged>
bluetooth_detailed_view_controller_;
FakeBluetoothDetailedViewFactory bluetooth_detailed_view_factory_;
FakeBluetoothDeviceListControllerFactory
bluetooth_device_list_controller_factory_;
std::unique_ptr<HidPreservingBluetoothStateControllerTestHelper>
hid_preserving_bluetooth_state_test_helper_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(BluetoothDetailedViewControllerConnectWarningTest,
ChangesBluetoothEnabledStateWhenTogglePressed) {
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(0u, GetTryToShowSurveyCount());
EXPECT_EQ(0u, GetDialogShownCount());
bluetooth_detailed_view_delegate()->OnToggleClicked(/*new_state=*/false);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
EXPECT_EQ(0u, GetDialogShownCount());
bluetooth_detailed_view_delegate()->OnToggleClicked(/*new_state=*/true);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kEnabling, GetBluetoothAdapterState());
EXPECT_EQ(2u, GetTryToShowSurveyCount());
EXPECT_EQ(0u, GetDialogShownCount());
}
TEST_F(BluetoothDetailedViewControllerConnectWarningTest,
SimulateShowWarningDialogOnDisconnect_ResultTrue) {
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(0u, GetTryToShowSurveyCount());
EXPECT_EQ(0u, GetDialogShownCount());
SetShouldShowWarningDialog(true);
bluetooth_detailed_view_delegate()->OnToggleClicked(/*new_state=*/false);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
EXPECT_EQ(1u, GetDialogShownCount());
CompleteShowWarningDialog(/*show_dialog_result=*/true);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
EXPECT_EQ(1u, GetDialogShownCount());
}
TEST_F(BluetoothDetailedViewControllerConnectWarningTest,
SimulateShowWarningDialogOnDisconnect_ResultFalse) {
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(0u, GetTryToShowSurveyCount());
EXPECT_EQ(0u, GetDialogShownCount());
SetShouldShowWarningDialog(true);
bluetooth_detailed_view_delegate()->OnToggleClicked(/*new_state=*/false);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
EXPECT_EQ(1u, GetDialogShownCount());
CompleteShowWarningDialog(/*show_dialog_result=*/false);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
EXPECT_EQ(1u, GetTryToShowSurveyCount());
EXPECT_EQ(1u, GetDialogShownCount());
}
} // namespace ash