0

[CrOS Bluetooth] Add DisableBluetoothDialogController

This CL adds a new class DisableBluetoothDialogController and its
implementation. This class would be used to create and show Bluetooth
warning dialog.

Video: https://photos.app.goo.gl/TGcoEpVkCdX1Ssmt5

Fixed: b/319321455
Test: Deployed to DUT, added unittest
Change-Id: Id2fb534aab75ba46018837a8eedfd526556140bd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5224602
Reviewed-by: Gordon Seto <gordonseto@google.com>
Commit-Queue: Theo Johnson-kanu <tjohnsonkanu@google.com>
Reviewed-by: Jason Zhang <jiajunz@google.com>
Cr-Commit-Position: refs/heads/main@{#1254037}
This commit is contained in:
Theo Johnson-Kanu
2024-01-30 18:19:53 +00:00
committed by Chromium LUCI CQ
parent 8e03f86243
commit d36147041d
13 changed files with 338 additions and 0 deletions

@ -1435,6 +1435,9 @@ component("ash") {
"system/bluetooth/bluetooth_notification_controller.h",
"system/bluetooth/bluetooth_state_cache.cc",
"system/bluetooth/bluetooth_state_cache.h",
"system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller.h",
"system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller_impl.cc",
"system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller_impl.h",
"system/brightness/brightness_controller_chromeos.cc",
"system/brightness/brightness_controller_chromeos.h",
"system/brightness/display_detailed_view.cc",
@ -3552,6 +3555,7 @@ test("ash_unittests") {
"system/bluetooth/fake_bluetooth_detailed_view.h",
"system/bluetooth/fake_bluetooth_device_list_controller.cc",
"system/bluetooth/fake_bluetooth_device_list_controller.h",
"system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller_unittest.cc",
"system/brightness/display_detailed_view_unittest.cc",
"system/brightness/unified_brightness_view_unittest.cc",
"system/camera/autozoom_feature_pod_controller_unittest.cc",

@ -3254,6 +3254,26 @@ Some features are limited to increase battery life.
Reject
</message>
<!-- Ash Bluetooth disconnect warning dialog strings -->
<message name="IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_TITLE" desc="The title for disconnect Bluetooth warning dialog.">
Turn off Bluetooth?
</message>
<message name="IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_TURN_OFF" desc="The label for turn off Bluetooth button for disconnect Bluetooth warning dialog.">
Turn off
</message>
<message name="IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_KEEP_ON" desc="The label for keep Bluetooth on button for disconnect Bluetooth warning dialog.">
Keep on
</message>
<message name="IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_DESCRIPTION_ONE_DEVICE" desc="Bluetooth pairing message shown to indicate a Bluetooth device will disconnect when Bluetooth is turned off.">
When you turn off Bluetooth, "<ph name="DEVICE_NAME">$1<ex>Dell RGB Keyboard</ex></ph>" will disconnect from your Chromebook.
</message>
<message name="IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_DESCRIPTION_TWO_DEVICES" desc="Bluetooth pairing message shown to indicate that two Bluetooth devices will disconnect when Bluetooth is turned off.">
When you turn off Bluetooth, "<ph name="DEVICE_NAME_1">$1<ex>Logitech SmartMouse</ex></ph>" and "<ph name="DEVICE_NAME_2">$2<ex>Dell RGB Keyboard</ex></ph>" will disconnect from your Chromebook.
</message>
<message name="IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_DESCRIPTION_MULTIPLE_DEVICES" desc="Bluetooth pairing message shown to indicate that multiple Bluetooth devices will disconnect when Bluetooth is turned off.">
When you turn off Bluetooth, "<ph name="DEVICE_NAME_1">$1<ex>Logitech SmartMouse</ex></ph>", "<ph name="DEVICE_NAME_2">$2<ex>Dell RGB Keyboard</ex></ph>" and <ph name="DEVICE_COUNT">$3<ex>3</ex></ph> other devices will disconnect from your Chromebook.
</message>
<!-- Ash multi-user warning dialog -->
<message name="IDS_DESKTOP_CASTING_ACTIVE_TITLE" desc="The title for the dialog which tells the user that desktop casting is in progress, asking if the casting should be stopped before switching - or - the switch should be aborted.">
Stop screen sharing?

@ -0,0 +1 @@
533b9663aa5f0e762b7d5d669352fadd6e3554c8

@ -0,0 +1 @@
533b9663aa5f0e762b7d5d669352fadd6e3554c8

@ -0,0 +1 @@
533b9663aa5f0e762b7d5d669352fadd6e3554c8

@ -0,0 +1 @@
533b9663aa5f0e762b7d5d669352fadd6e3554c8

@ -0,0 +1 @@
533b9663aa5f0e762b7d5d669352fadd6e3554c8

@ -0,0 +1 @@
533b9663aa5f0e762b7d5d669352fadd6e3554c8

@ -935,6 +935,7 @@ COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBackgroundListeningEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBatterySaverAvailable();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBatterySaverAlwaysOn();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBluetoothQualityReportEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBluetoothDisconnectWarningEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCaptureModeAudioMixingEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCaptureModeEducationEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS)

@ -0,0 +1,37 @@
// 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.
#ifndef ASH_SYSTEM_BLUETOOTH_HID_PRESERVING_CONTROLLER_DISABLE_BLUETOOTH_DIALOG_CONTROLLER_H_
#define ASH_SYSTEM_BLUETOOTH_HID_PRESERVING_CONTROLLER_DISABLE_BLUETOOTH_DIALOG_CONTROLLER_H_
#include <stdint.h>
#include "ash/ash_export.h"
namespace ash {
// The DisableBluetoothDialogController displays a UI Blocking dialog which
// warns that disabling Bluetooth will disconnect currently paired HID input
// devices.
class ASH_EXPORT DisableBluetoothDialogController {
public:
DisableBluetoothDialogController() = default;
DisableBluetoothDialogController(const DisableBluetoothDialogController&) =
delete;
DisableBluetoothDialogController& operator=(
const DisableBluetoothDialogController&) = delete;
virtual ~DisableBluetoothDialogController() = default;
using ShowDialogCallback = base::OnceCallback<void(bool)>;
using DeviceNamesList = std::vector<std::string>;
virtual void ShowDialog(const DeviceNamesList& devices,
ShowDialogCallback callback) = 0;
};
} // namespace ash
#endif // ASH_SYSTEM_BLUETOOTH_HID_PRESERVING_CONTROLLER_DISABLE_BLUETOOTH_DIALOG_CONTROLLER_H_

@ -0,0 +1,117 @@
// 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/disable_bluetooth_dialog_controller_impl.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/widget/widget.h"
namespace ash {
DisableBluetoothDialogControllerImpl::DisableBluetoothDialogControllerImpl() {
CHECK(features::IsBluetoothDisconnectWarningEnabled());
}
DisableBluetoothDialogControllerImpl::~DisableBluetoothDialogControllerImpl() {
if (dialog_widget_ && !dialog_widget_->IsClosed()) {
dialog_widget_->CloseNow();
}
}
void DisableBluetoothDialogControllerImpl::ShowDialog(
const DeviceNamesList& devices,
DisableBluetoothDialogController::ShowDialogCallback callback) {
show_dialog_callback_ = std::move(callback);
// We should not get here with an active dialog.
DCHECK_EQ(dialog_widget_, nullptr);
CHECK(!devices.empty());
std::u16string dialog_description;
if (devices.size() == 1) {
dialog_description = l10n_util::GetStringFUTF16(
IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_DESCRIPTION_ONE_DEVICE,
base::UTF8ToUTF16(devices[0]));
} else if (devices.size() == 2) {
dialog_description = l10n_util::GetStringFUTF16(
IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_DESCRIPTION_TWO_DEVICES,
base::UTF8ToUTF16(devices[0]), base::UTF8ToUTF16(devices[1]));
} else {
dialog_description = l10n_util::GetStringFUTF16(
IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_DESCRIPTION_MULTIPLE_DEVICES,
base::UTF8ToUTF16(devices[0]), base::UTF8ToUTF16(devices[1]),
base::NumberToString16(devices.size() - 2));
}
auto dialog = views::Builder<SystemDialogDelegateView>()
.SetTitleText(l10n_util::GetStringUTF16(
IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_TITLE))
.SetDescription(dialog_description)
.SetAcceptButtonText(l10n_util::GetStringUTF16(
IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_TURN_OFF))
.SetCancelButtonText(l10n_util::GetStringUTF16(
IDS_ASH_DISCONNECT_BLUETOOTH_WARNING_DIALOG_KEEP_ON))
.SetCancelCallback(base::BindOnce(
&DisableBluetoothDialogControllerImpl::OnCancelDialog,
weak_ptr_factory_.GetWeakPtr()))
.SetAcceptCallback(base::BindOnce(
&DisableBluetoothDialogControllerImpl::OnConfirmDialog,
weak_ptr_factory_.GetWeakPtr()))
.Build();
dialog->SetModalType(ui::MODAL_TYPE_SYSTEM);
dialog->SetShowCloseButton(false);
views::Widget::InitParams params;
params.context = Shell::GetPrimaryRootWindow();
params.delegate = dialog.release();
params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
dialog_widget_ = new views::Widget();
dialog_widget_->Init(std::move(params));
dialog_widget_->Show();
dialog_widget_observation_.Observe(dialog_widget_.get());
// Ensure that if ChromeVox is enabled, it focuses on the dialog.
AccessibilityController* accessibility_controller =
Shell::Get()->accessibility_controller();
if (accessibility_controller->spoken_feedback().enabled()) {
accessibility_controller->SetA11yOverrideWindow(
dialog_widget_->GetNativeWindow());
}
}
void DisableBluetoothDialogControllerImpl::OnWidgetDestroying(
views::Widget* widget) {
DCHECK_EQ(dialog_widget_, widget);
dialog_widget_observation_.Reset();
dialog_widget_ = nullptr;
}
void DisableBluetoothDialogControllerImpl::OnConfirmDialog() {
CHECK(show_dialog_callback_);
std::move(show_dialog_callback_).Run(true);
}
void DisableBluetoothDialogControllerImpl::OnCancelDialog() {
CHECK(show_dialog_callback_);
std::move(show_dialog_callback_).Run(false);
}
const SystemDialogDelegateView*
DisableBluetoothDialogControllerImpl::GetSystemDialogViewForTesting() const {
CHECK(dialog_widget_);
return static_cast<SystemDialogDelegateView*>(
dialog_widget_->GetContentsView());
}
} // namespace ash

@ -0,0 +1,60 @@
// 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.
#ifndef ASH_SYSTEM_BLUETOOTH_HID_PRESERVING_CONTROLLER_DISABLE_BLUETOOTH_DIALOG_CONTROLLER_IMPL_H_
#define ASH_SYSTEM_BLUETOOTH_HID_PRESERVING_CONTROLLER_DISABLE_BLUETOOTH_DIALOG_CONTROLLER_IMPL_H_
#include "ash/ash_export.h"
#include "ash/style/system_dialog_delegate_view.h"
#include "ash/system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
namespace ash {
class ASH_EXPORT DisableBluetoothDialogControllerImpl
: public DisableBluetoothDialogController,
public views::WidgetObserver {
public:
DisableBluetoothDialogControllerImpl();
DisableBluetoothDialogControllerImpl(
const DisableBluetoothDialogControllerImpl&) = delete;
DisableBluetoothDialogControllerImpl& operator=(
const DisableBluetoothDialogControllerImpl&) = delete;
~DisableBluetoothDialogControllerImpl() override;
// DisableBluetoothDialogController:
void ShowDialog(const DeviceNamesList& devices,
ShowDialogCallback callback) override;
// views::WidgetObserver:
void OnWidgetDestroying(views::Widget* widget) override;
private:
friend class DisableBluetoothDialogControllerTest;
void OnConfirmDialog();
void OnCancelDialog();
const SystemDialogDelegateView* GetSystemDialogViewForTesting() const;
DisableBluetoothDialogController::ShowDialogCallback show_dialog_callback_;
// Pointer to the widget (if any) that contains the current dialog.
raw_ptr<views::Widget> dialog_widget_ = nullptr;
base::ScopedObservation<views::Widget, views::WidgetObserver>
dialog_widget_observation_{this};
base::WeakPtrFactory<DisableBluetoothDialogControllerImpl> weak_ptr_factory_{
this};
};
} // namespace ash
#endif // ASH_SYSTEM_BLUETOOTH_HID_PRESERVING_CONTROLLER_DISABLE_BLUETOOTH_DIALOG_CONTROLLER_IMPL_H_

@ -0,0 +1,93 @@
// 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/constants/ash_features.h"
#include "ash/system/bluetooth/hid_preserving_controller/disable_bluetooth_dialog_controller_impl.h"
#include "ash/test/ash_test_base.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/views/test/widget_test.h"
namespace ash {
class DisableBluetoothDialogControllerTest : public AshTestBase {
public:
public:
void SetUp() override {
AshTestBase::SetUp();
scoped_feature_list_.InitAndEnableFeature(
features::kBluetoothDisconnectWarning);
disable_bluetooth_dialog_controller_impl_ =
std::make_unique<DisableBluetoothDialogControllerImpl>();
}
void TearDown() override { AshTestBase::TearDown(); }
const views::View* GetDialogAcceptButton() {
return disable_bluetooth_dialog_controller_impl_
->GetSystemDialogViewForTesting()
->GetAcceptButtonForTesting();
}
const views::View* GetDialogCancelButton() {
return disable_bluetooth_dialog_controller_impl_
->GetSystemDialogViewForTesting()
->GetCancelButtonForTesting();
}
void WaitAndAssertWidgetIsDestroyed() {
views::test::WidgetDestroyedWaiter(
disable_bluetooth_dialog_controller_impl_->dialog_widget_)
.Wait();
ASSERT_FALSE(disable_bluetooth_dialog_controller_impl_->dialog_widget_);
}
void ShowDialog(
const std::vector<std::string>& deviceNames,
DisableBluetoothDialogController::ShowDialogCallback callback) {
disable_bluetooth_dialog_controller_impl_->ShowDialog(deviceNames,
std::move(callback));
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<DisableBluetoothDialogControllerImpl>
disable_bluetooth_dialog_controller_impl_;
};
TEST_F(DisableBluetoothDialogControllerTest, OnAcceptButtonClicked) {
std::vector<std::string> device_names = {"RGB Keyboard", "MX Keys",
"Logitech SmartMouse"};
UNCALLED_MOCK_CALLBACK(DisableBluetoothDialogController::ShowDialogCallback,
callback);
ShowDialog(device_names, callback.Get());
// Click the accept button. `callback` should be called.
const views::View* const accept_button = GetDialogAcceptButton();
ASSERT_TRUE(accept_button);
EXPECT_CALL(callback, Run(true));
LeftClickOn(accept_button);
WaitAndAssertWidgetIsDestroyed();
}
TEST_F(DisableBluetoothDialogControllerTest, OnCancelButtonClicked) {
std::vector<std::string> device_names = {"RGB Keyboard", "MX Keys",
"Logitech SmartMouse"};
UNCALLED_MOCK_CALLBACK(DisableBluetoothDialogController::ShowDialogCallback,
callback);
ShowDialog(device_names, callback.Get());
// Click the accept button. `callback` should be called.
const views::View* const cancel_button = GetDialogCancelButton();
ASSERT_TRUE(cancel_button);
EXPECT_CALL(callback, Run(false));
LeftClickOn(cancel_button);
WaitAndAssertWidgetIsDestroyed();
}
} // namespace ash