#include "ash/system/accessibility/dictation_button_tray.h"
#include <memory>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/test_accessibility_controller_client.h"
#include "ash/constants/ash_features.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/login_status.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/rotator/screen_rotation_animator.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/system/progress_indicator/progress_indicator.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_helper.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "components/soda/soda_installer.h"
#include "components/soda/soda_installer_impl_chromeos.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/input_method_ash.h"
#include "ui/base/ime/fake_text_input_client.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_provider.h"
#include "ui/display/display_switches.h"
#include "ui/display/manager/display_manager.h"
#include "ui/events/event.h"
#include "ui/events/gestures/gesture_types.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
const std::string kEnabledTooltip = "Dictation";
const std::string kDisabledTooltip = "Downloading speech files";
DictationButtonTray* GetTray() {
return StatusAreaWidgetTestHelper::GetStatusAreaWidget()
->dictation_button_tray();
}
class ProgressIndicatorWaiter {
public:
ProgressIndicatorWaiter() = default;
ProgressIndicatorWaiter(const ProgressIndicatorWaiter&) = delete;
ProgressIndicatorWaiter& operator=(const ProgressIndicatorWaiter&) = delete;
~ProgressIndicatorWaiter() = default;
void WaitForProgress(ProgressIndicator* progress_indicator,
const std::optional<float>& progress) {
if (progress_indicator->progress() == progress)
return;
base::RunLoop run_loop;
auto subscription = progress_indicator->AddProgressChangedCallback(
base::BindLambdaForTesting([&]() {
if (progress_indicator->progress() == progress)
run_loop.Quit();
}));
run_loop.Run();
}
};
}
class DictationButtonTrayTest : public AshTestBase {
public:
DictationButtonTrayTest() = default;
~DictationButtonTrayTest() override = default;
DictationButtonTrayTest(const DictationButtonTrayTest&) = delete;
DictationButtonTrayTest& operator=(const DictationButtonTrayTest&) = delete;
void SetUp() override {
fake_text_input_client_ =
std::make_unique<ui::FakeTextInputClient>(ui::TEXT_INPUT_TYPE_TEXT);
InputMethodAsh ime(nullptr);
IMEBridge::Get()->SetInputContextHandler(&ime);
AshTestBase::SetUp();
FocusTextInputClient();
}
protected:
views::ImageView* GetImageView(DictationButtonTray* tray) {
return tray->icon_;
}
void CheckDictationStatusAndUpdateIcon(DictationButtonTray* tray) {
tray->CheckDictationStatusAndUpdateIcon();
}
void FocusTextInputClient() {
Shell::Get()
->window_tree_host_manager()
->input_method()
->SetFocusedTextInputClient(fake_text_input_client_.get());
}
void DetachTextInputClient() {
Shell::Get()
->window_tree_host_manager()
->input_method()
->SetFocusedTextInputClient(nullptr);
}
std::unique_ptr<ui::FakeTextInputClient> fake_text_input_client_;
};
TEST_F(DictationButtonTrayTest, BasicConstruction) {
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
controller->dictation().SetEnabled(true);
EXPECT_TRUE(GetImageView(GetTray()));
EXPECT_TRUE(GetTray()->GetVisible());
ui::AXNodeData tray_data;
GetTray()->GetViewAccessibility().GetAccessibleNodeData(&tray_data);
EXPECT_EQ(
tray_data.GetString16Attribute(ax::mojom::StringAttribute::kName),
l10n_util::GetStringUTF16(IDS_ASH_DICTATION_BUTTON_ACCESSIBLE_NAME));
}
TEST_F(DictationButtonTrayTest, ButtonActivatesDictation) {
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
TestAccessibilityControllerClient client;
controller->dictation().SetEnabled(true);
EXPECT_FALSE(controller->dictation_active());
GestureTapOn(GetTray());
EXPECT_TRUE(controller->dictation_active());
GestureTapOn(GetTray());
EXPECT_FALSE(controller->dictation_active());
}
TEST_F(DictationButtonTrayTest, ActivatingDictationActivatesButton) {
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
controller->dictation().SetEnabled(true);
Shell::Get()->OnDictationStarted();
EXPECT_TRUE(GetTray()->is_active());
Shell::Get()->OnDictationEnded();
EXPECT_FALSE(GetTray()->is_active());
}
TEST_F(DictationButtonTrayTest, ActiveStateOnlyDuringDictation) {
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
TestAccessibilityControllerClient client;
controller->dictation().SetEnabled(true);
ASSERT_FALSE(controller->dictation_active());
ASSERT_FALSE(GetTray()->is_active());
EXPECT_TRUE(GetTray()->GetEnabled());
Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
AcceleratorAction::kEnableOrToggleDictation, {});
EXPECT_TRUE(controller->dictation_active());
EXPECT_TRUE(GetTray()->is_active());
Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
AcceleratorAction::kEnableOrToggleDictation, {});
EXPECT_FALSE(controller->dictation_active());
EXPECT_FALSE(GetTray()->is_active());
}
TEST_F(DictationButtonTrayTest, ImageIcons) {
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
TestAccessibilityControllerClient client;
controller->dictation().SetEnabled(true);
const auto* color_provider = GetTray()->GetColorProvider();
const auto off_icon_color =
color_provider->GetColor(cros_tokens::kCrosSysOnSurface);
const auto on_icon_color =
color_provider->GetColor(cros_tokens::kCrosSysSystemOnPrimaryContainer);
gfx::ImageSkia off_icon =
gfx::CreateVectorIcon(kDictationOffNewuiIcon, off_icon_color);
gfx::ImageSkia on_icon =
gfx::CreateVectorIcon(kDictationOnNewuiIcon, on_icon_color);
views::ImageView* view = GetImageView(GetTray());
EXPECT_TRUE(gfx::test::AreBitmapsEqual(*view->GetImage().bitmap(),
*off_icon.bitmap()));
Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
AcceleratorAction::kEnableOrToggleDictation, {});
EXPECT_TRUE(gfx::test::AreBitmapsEqual(*view->GetImage().bitmap(),
*on_icon.bitmap()));
}
TEST_F(DictationButtonTrayTest, DisabledWhenNoInputFocused) {
DetachTextInputClient();
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
controller->dictation().SetEnabled(true);
DictationButtonTray* tray = GetTray();
EXPECT_FALSE(tray->GetEnabled());
Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
AcceleratorAction::kEnableOrToggleDictation, {});
EXPECT_FALSE(controller->dictation_active());
EXPECT_FALSE(tray->GetEnabled());
FocusTextInputClient();
EXPECT_TRUE(tray->GetEnabled());
DetachTextInputClient();
EXPECT_FALSE(tray->GetEnabled());
}
class DictationButtonTraySodaTest : public DictationButtonTrayTest {
public:
DictationButtonTraySodaTest() = default;
~DictationButtonTraySodaTest() override = default;
DictationButtonTraySodaTest(const DictationButtonTraySodaTest&) = delete;
DictationButtonTraySodaTest& operator=(const DictationButtonTraySodaTest&) =
delete;
void SetUp() override {
DictationButtonTrayTest::SetUp();
scoped_feature_list_.InitAndEnableFeature(
features::kOnDeviceSpeechRecognition);
soda_installer_impl_ =
std::make_unique<speech::SodaInstallerImplChromeOS>();
}
void TearDown() override {
soda_installer_impl_.reset();
AshTestBase::TearDown();
}
ProgressIndicator* GetProgressIndicator() {
return GetTray()->progress_indicator_.get();
}
float GetProgressIndicatorProgress() const {
DCHECK(GetTray()->progress_indicator_);
std::optional<float> progress = GetTray()->progress_indicator_->progress();
DCHECK(progress.has_value());
return progress.value();
}
bool IsImageVisible() {
DCHECK(GetTray()->icon_);
ui::Layer* const layer = GetTray()->icon_->layer();
if (!layer)
return true;
return layer->GetTargetOpacity() == 1.f &&
layer->GetTargetTransform() == gfx::Transform();
}
bool IsProgressIndicatorVisible() const {
const float progress = GetProgressIndicatorProgress();
return progress > 0.f && progress < 1.f;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<speech::SodaInstallerImplChromeOS> soda_installer_impl_;
};
TEST_F(DictationButtonTraySodaTest, UpdateOnSpeechRecognitionDownloadChanged) {
AccessibilityController* controller =
Shell::Get()->accessibility_controller();
controller->dictation().SetEnabled(true);
DictationButtonTray* tray = GetTray();
views::ImageView* image = GetImageView(tray);
EXPECT_TRUE(IsImageVisible());
tray->UpdateOnSpeechRecognitionDownloadChanged(0);
EXPECT_EQ(0, tray->download_progress());
EXPECT_TRUE(tray->GetEnabled());
EXPECT_EQ(base::UTF8ToUTF16(kEnabledTooltip), image->GetTooltipText());
ProgressIndicator* progress_indicator = GetProgressIndicator();
ProgressIndicatorWaiter().WaitForProgress(
progress_indicator, ProgressIndicator::kProgressComplete);
EXPECT_FALSE(IsProgressIndicatorVisible());
EXPECT_TRUE(IsImageVisible());
tray->UpdateOnSpeechRecognitionDownloadChanged(50);
EXPECT_EQ(50, tray->download_progress());
EXPECT_FALSE(tray->GetEnabled());
EXPECT_EQ(base::UTF8ToUTF16(kDisabledTooltip), image->GetTooltipText());
DetachTextInputClient();
EXPECT_FALSE(tray->GetEnabled());
FocusTextInputClient();
EXPECT_FALSE(tray->GetEnabled());
ProgressIndicatorWaiter().WaitForProgress(progress_indicator, 0.5f);
EXPECT_TRUE(IsProgressIndicatorVisible());
EXPECT_FALSE(progress_indicator->inner_icon_visible());
EXPECT_TRUE(IsImageVisible());
tray->UpdateOnSpeechRecognitionDownloadChanged(70);
EXPECT_EQ(70, tray->download_progress());
EXPECT_FALSE(tray->GetEnabled());
EXPECT_EQ(base::UTF8ToUTF16(kDisabledTooltip), image->GetTooltipText());
ProgressIndicatorWaiter().WaitForProgress(progress_indicator, 0.7f);
EXPECT_TRUE(IsProgressIndicatorVisible());
EXPECT_FALSE(progress_indicator->inner_icon_visible());
EXPECT_TRUE(IsImageVisible());
tray->UpdateOnSpeechRecognitionDownloadChanged(100);
EXPECT_EQ(100, tray->download_progress());
EXPECT_TRUE(tray->GetEnabled());
EXPECT_EQ(base::UTF8ToUTF16(kEnabledTooltip), image->GetTooltipText());
ProgressIndicatorWaiter().WaitForProgress(
progress_indicator, ProgressIndicator::kProgressComplete);
EXPECT_FALSE(IsProgressIndicatorVisible());
EXPECT_TRUE(IsImageVisible());
}
}