chromium/ash/system/bluetooth/hid_preserving_controller/hid_preserving_bluetooth_state_controller_unittest.cc

// Copyright 2024 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/hid_preserving_controller/hid_preserving_bluetooth_state_controller.h"

#include "ash/constants/ash_features.h"
#include "ash/system/bluetooth/hid_preserving_controller/fake_disable_bluetooth_dialog_controller.h"
#include "ash/system/bluetooth/hid_preserving_controller/hid_preserving_bluetooth_metrics.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_helper.h"
#include "base/files/file_path.h"
#include "base/test/metrics/histogram_tester.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/public/mojom/cros_bluetooth_config.mojom.h"
#include "chromeos/ash/services/bluetooth_config/scoped_bluetooth_config_test_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/devices/device_data_manager_test_api.h"
#include "ui/events/devices/touchpad_device.h"
#include "ui/events/devices/touchscreen_device.h"
#include "ui/gfx/geometry/size.h"

namespace ash {

namespace {
using bluetooth_config::ScopedBluetoothConfigTestHelper;
using bluetooth_config::mojom::BluetoothSystemState;

// Logitech Vendor ID
const uint16_t kLogitechVID = 0x046d;

// Logitech MX Master 3S Product ID (Bluetooth)
const uint16_t KMousePID = 0xb034;

// Logitech MX Keys Product ID (Bluetooth)
const uint16_t KKeyboardPID = 0xb35b;

const std::string kSampleMouseBluetooth = "kSampleMouseBluetooth";
const std::string kSampleKeyboardBluetooth = "kSampleKeyboardBluetooth";

const ui::KeyboardDevice GetSampleKeyboardBluetooth() {
  return {10,
          ui::INPUT_DEVICE_BLUETOOTH,
          kSampleKeyboardBluetooth,
          /* phys= */ "",
          base::FilePath(),
          kLogitechVID,
          KKeyboardPID,
          /* version= */ 0};
}

const ui::InputDevice GetSampleMouseBluetoothDuplicate() {
  return {110,
          ui::INPUT_DEVICE_BLUETOOTH,
          "kSampleMouseBluetoothDuplicate",
          /* phys= */ "",
          base::FilePath(),
          kLogitechVID,
          KKeyboardPID,
          /* version= */ 0};
}

const ui::KeyboardDevice GetSampleKeyboardUsb() {
  return {15, ui::INPUT_DEVICE_USB, "kSampleKeyboardUsb"};
}

const ui::InputDevice GetSampleMouseUsb() {
  return {20, ui::INPUT_DEVICE_USB, "kSampleMouseUsb"};
}

const ui::InputDevice GetSampleMouseBluetooth() {
  return {25,
          ui::INPUT_DEVICE_BLUETOOTH,
          kSampleMouseBluetooth,
          /* phys= */ "",
          base::FilePath(),
          kLogitechVID,
          KMousePID,
          /* version= */ 0};
}

const ui::KeyboardDevice GetSampleKeyboardBluetoothDuplicate() {
  return {100,
          ui::INPUT_DEVICE_BLUETOOTH,
          "kSampleKeyboardBluetoothDuplicate",
          /* phys= */ "",
          base::FilePath(),
          kLogitechVID,
          KMousePID,
          /* version= */ 0};
}

const ui::KeyboardDevice GetSampleMouseInternal() {
  return {30, ui::INPUT_DEVICE_INTERNAL, "kSampleMouseInternal"};
}

const ui::TouchpadDevice GetSampleTouchpadInternal() {
  return {35, ui::InputDeviceType::INPUT_DEVICE_INTERNAL,
          "kSampleTouchpadInternal"};
}

const ui::TouchpadDevice GetSampleTouchpadBluetooth() {
  return {40, ui::InputDeviceType::INPUT_DEVICE_BLUETOOTH,
          "kSampleTouchpadBluetooth"};
}

const ui::TouchscreenDevice GetSampleTouchscreenBluetooth() {
  return {45, ui::InputDeviceType::INPUT_DEVICE_BLUETOOTH,
          "kSampleTouchscreenBluetooth", gfx::Size(123, 456), 1};
}

const ui::TouchscreenDevice GetSampleTouchscreenInternal() {
  return {50, ui::InputDeviceType::INPUT_DEVICE_INTERNAL,
          "kSampleTouchscreenInternal", gfx::Size(123, 456), 1};
}

const ui::InputDevice GetSamplePointingStickBluetooth() {
  return {55, ui::INPUT_DEVICE_BLUETOOTH, "kSamplePointingStickBluetooth"};
}

const ui::InputDevice GetSamplePointingStickInternal() {
  return {60, ui::INPUT_DEVICE_INTERNAL, "kSamplePointingStickInternal"};
}

const ui::InputDevice GetSampleGraphicsTabletBluetooth() {
  return {65, ui::INPUT_DEVICE_BLUETOOTH, "kSampleGraphicsTabletBluetooth"};
}

const ui::InputDevice GetSampleGraphicsTabletInternal() {
  return {70, ui::INPUT_DEVICE_INTERNAL, "kSampleGraphicsTabletInternal"};
}

}  // namespace

class HidPreservingBluetoothStateControllerTest : public AshTestBase {
 public:
  struct ExpectedHistogramState {
    size_t disabled_bluetooth_dialog_shown_count = 0u;
    size_t disabled_bluetooth_dialog_not_shown_count = 0u;
    size_t user_action_keep_on_count = 0u;
    size_t user_action_turn_off_count = 0u;
    size_t disabled_bluetooth_dialog_source_os_settings_count = 0u;
    size_t disabled_bluetooth_dialog_source_quick_settings_count = 0u;
  };

  void SetUp() override {
    AshTestBase::SetUp();

    scoped_feature_list_.InitAndEnableFeature(
        features::kBluetoothDisconnectWarning);

    hid_preserving_bluetooth_state_controller_ =
        std::make_unique<HidPreservingBluetoothStateController>();

    std::unique_ptr<FakeDisableBluetoothDialogController>
        disable_bluetooth_dialog_controller =
            std::make_unique<FakeDisableBluetoothDialogController>();

    hid_preserving_bluetooth_state_controller_
        ->SetDisableBluetoothDialogControllerForTest(
            std::move(disable_bluetooth_dialog_controller));
  }

  void SetBluetoothAdapterState(BluetoothSystemState system_state) {
    bluetooth_config_test_helper()
        ->fake_adapter_state_controller()
        ->SetSystemState(system_state);
    base::RunLoop().RunUntilIdle();
  }

  void TryToSetBluetoothEnabledState(bool enabled,
                                     mojom::HidWarningDialogSource source) {
    hid_preserving_bluetooth_state_controller_->TryToSetBluetoothEnabledState(
        enabled, source);
    base::RunLoop().RunUntilIdle();
  }

  void CompleteShowDialog(size_t called_count, bool show_dialog_result) {
    EXPECT_EQ(GetDisabledBluetoothDialogController()->show_dialog_call_count(),
              called_count);
    GetDisabledBluetoothDialogController()->CompleteShowDialogCallback(
        show_dialog_result);
  }

  FakeDisableBluetoothDialogController* GetDisabledBluetoothDialogController() {
    return static_cast<FakeDisableBluetoothDialogController*>(
        hid_preserving_bluetooth_state_controller_
            ->GetDisabledBluetoothDialogForTesting());
  }

  BluetoothSystemState GetBluetoothAdapterState() {
    return bluetooth_config_test_helper()
        ->fake_adapter_state_controller()
        ->GetAdapterState();
  }

  DisableBluetoothDialogController::DeviceNamesList* GetShownDevicesList() {
    return hid_preserving_bluetooth_state_controller_
        ->device_names_for_testing();
  }

  void CheckHistogramState(const ExpectedHistogramState& state) {
    histogram_tester_.ExpectBucketCount(
        bluetooth::kPoweredDisableDialogBehavior,
        bluetooth::DisabledBehavior::kWarningDialogShown,
        state.disabled_bluetooth_dialog_shown_count);
    histogram_tester_.ExpectBucketCount(
        bluetooth::kPoweredDisableDialogBehavior,
        bluetooth::DisabledBehavior::kWarningDialogNotShown,
        state.disabled_bluetooth_dialog_not_shown_count);
    histogram_tester_.ExpectBucketCount(bluetooth::kUserAction,
                                        bluetooth::UserAction::kKeepOn,
                                        state.user_action_keep_on_count);
    histogram_tester_.ExpectBucketCount(bluetooth::kUserAction,
                                        bluetooth::UserAction::kTurnOff,
                                        state.user_action_turn_off_count);
    histogram_tester_.ExpectBucketCount(
        bluetooth::kDialogSource, bluetooth::DialogSource::kQuickSettings,
        state.disabled_bluetooth_dialog_source_quick_settings_count);
    histogram_tester_.ExpectBucketCount(
        bluetooth::kDialogSource, bluetooth::DialogSource::kOsSettings,
        state.disabled_bluetooth_dialog_source_os_settings_count);
  }

 private:
  ScopedBluetoothConfigTestHelper* bluetooth_config_test_helper() {
    return ash_test_helper()->bluetooth_config_test_helper();
  }

  base::HistogramTester histogram_tester_;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<HidPreservingBluetoothStateController>
      hid_preserving_bluetooth_state_controller_;
};

TEST_F(HidPreservingBluetoothStateControllerTest, BluetoothEnabled) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kDisabled);
  EXPECT_EQ(BluetoothSystemState::kDisabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  TryToSetBluetoothEnabledState(/*enabled=*/true,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kEnabling, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithTouchScreenDevice) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());

  std::vector<ui::TouchscreenDevice> screens;
  screens.push_back(
      ui::TouchscreenDevice(1, ui::InputDeviceType::INPUT_DEVICE_INTERNAL,
                            "Touchscreen", gfx::Size(1024, 768), 0));
  ui::DeviceDataManagerTestApi().SetTouchscreenDevices(screens);

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithMouseDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetMouseDevices({GetSampleMouseUsb(),
                                                  GetSampleMouseBluetooth(),
                                                  GetSampleMouseInternal()});
  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithDuplicateMouseDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetMouseDevices({GetSampleMouseBluetooth()});
  ui::DeviceDataManagerTestApi().SetKeyboardDevices(
      {GetSampleKeyboardBluetoothDuplicate()});

  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_source_quick_settings_count++;
  expected_state.disabled_bluetooth_dialog_shown_count++;
  CheckHistogramState(expected_state);

  DisableBluetoothDialogController::DeviceNamesList* device_names =
      GetShownDevicesList();
  EXPECT_EQ(1u, device_names->size());
  EXPECT_EQ(device_names->front(), kSampleMouseBluetooth);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithDuplicateKeyboardDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetMouseDevices(
      {GetSampleMouseBluetoothDuplicate()});
  ui::DeviceDataManagerTestApi().SetKeyboardDevices(
      {GetSampleKeyboardBluetooth()});

  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_source_quick_settings_count++;
  expected_state.disabled_bluetooth_dialog_shown_count++;
  CheckHistogramState(expected_state);

  DisableBluetoothDialogController::DeviceNamesList* device_names =
      GetShownDevicesList();
  EXPECT_EQ(1u, device_names->size());
  EXPECT_EQ(device_names->front(), kSampleKeyboardBluetooth);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithTouchPadDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetTouchpadDevices(
      {GetSampleTouchpadInternal(), GetSampleTouchpadBluetooth()});
  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithKeyboardDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetKeyboardDevices(
      {GetSampleKeyboardBluetooth(), GetSampleKeyboardUsb()});
  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithTouchscreenDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetTouchscreenDevices(
      {GetSampleTouchscreenBluetooth(), GetSampleTouchscreenInternal()});
  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithPointingStickDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetPointingStickDevices(
      {GetSamplePointingStickBluetooth(), GetSamplePointingStickInternal()});
  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothNoBluetoothDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);
  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  TryToSetBluetoothEnabledState(false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);

  TryToSetBluetoothEnabledState(true,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kEnabling, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  TryToSetBluetoothEnabledState(false,
                                mojom::HidWarningDialogSource::kOsSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithOnlyBluetoothDevices_AllResults) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetMouseDevices({GetSampleMouseBluetooth()});
  ui::DeviceDataManagerTestApi().SetKeyboardDevices(
      {GetSampleKeyboardBluetooth()});
  ui::DeviceDataManagerTestApi().SetTouchpadDevices(
      {GetSampleTouchpadBluetooth()});
  ui::DeviceDataManagerTestApi().SetPointingStickDevices(
      {GetSamplePointingStickBluetooth()});
  ui::DeviceDataManagerTestApi().SetTouchscreenDevices(
      {GetSampleTouchscreenBluetooth()});
  ui::DeviceDataManagerTestApi().SetGraphicsTabletDevices(
      {GetSampleGraphicsTabletBluetooth()});
  base::RunLoop().RunUntilIdle();

  size_t called_count = 0u;
  // Try to set Bluetooth enabled state with different sources and results.
  for (const auto& [source, result] :
       std::vector<std::pair<mojom::HidWarningDialogSource, bool>>{
           {mojom::HidWarningDialogSource::kQuickSettings, true},
           {mojom::HidWarningDialogSource::kQuickSettings, false},
           {mojom::HidWarningDialogSource::kOsSettings, true},
           {mojom::HidWarningDialogSource::kOsSettings, false}}) {
    SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
    base::RunLoop().RunUntilIdle();
    TryToSetBluetoothEnabledState(false, source);
    expected_state.disabled_bluetooth_dialog_shown_count++;
    if (source == mojom::HidWarningDialogSource::kQuickSettings) {
      expected_state.disabled_bluetooth_dialog_source_quick_settings_count++;
    } else {
      expected_state.disabled_bluetooth_dialog_source_os_settings_count++;
    }
    CheckHistogramState(expected_state);

    CompleteShowDialog(++called_count, /*show_dialog_result=*/result);
    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(result ? BluetoothSystemState::kDisabling
                     : BluetoothSystemState::kEnabled,
              GetBluetoothAdapterState());

    expected_state.user_action_turn_off_count += result;
    expected_state.user_action_keep_on_count += !result;
    CheckHistogramState(expected_state);
  }
}

TEST_F(HidPreservingBluetoothStateControllerTest,
       DisableBluetoothWithGraphicsTabletDevices) {
  ExpectedHistogramState expected_state;
  CheckHistogramState(expected_state);

  SetBluetoothAdapterState(BluetoothSystemState::kEnabled);
  EXPECT_EQ(BluetoothSystemState::kEnabled, GetBluetoothAdapterState());
  CheckHistogramState(expected_state);

  ui::DeviceDataManagerTestApi().SetGraphicsTabletDevices(
      {GetSampleGraphicsTabletBluetooth(), GetSampleGraphicsTabletInternal()});
  base::RunLoop().RunUntilIdle();

  TryToSetBluetoothEnabledState(/*enabled=*/false,
                                mojom::HidWarningDialogSource::kQuickSettings);
  EXPECT_EQ(BluetoothSystemState::kDisabling, GetBluetoothAdapterState());
  expected_state.disabled_bluetooth_dialog_not_shown_count++;
  CheckHistogramState(expected_state);
}

}  // namespace ash