blob: 146ea49ad4072240877ed227e4688a92f4432bdc [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/arc/accessibility/arc_accessibility_helper_bridge.h"
#include <memory>
#include <unordered_map>
#include <utility>
#include "ash/system/message_center/arc/arc_notification_constants.h"
#include "ash/system/message_center/arc/arc_notification_content_view.h"
#include "ash/system/message_center/arc/arc_notification_surface.h"
#include "ash/system/message_center/arc/arc_notification_surface_manager.h"
#include "ash/system/message_center/arc/arc_notification_view.h"
#include "ash/system/message_center/arc/mock_arc_notification_item.h"
#include "ash/system/message_center/arc/mock_arc_notification_surface.h"
#include "base/command_line.h"
#include "base/observer_list.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "chromeos/chromeos_switches.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/common/accessibility_helper.mojom.h"
#include "components/exo/shell_surface.h"
#include "components/exo/shell_surface_util.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
#include "ui/display/display.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
using ash::ArcNotificationItem;
using ash::ArcNotificationSurface;
using ash::ArcNotificationSurfaceManager;
using ash::ArcNotificationView;
using ash::MockArcNotificationItem;
using ash::MockArcNotificationSurface;
namespace arc {
namespace {
constexpr char kNotificationKey[] = "unit.test.notification";
} // namespace
class ArcAccessibilityHelperBridgeTest : public ChromeViewsTestBase {
public:
class TestArcAccessibilityHelperBridge : public ArcAccessibilityHelperBridge {
public:
TestArcAccessibilityHelperBridge(content::BrowserContext* browser_context,
ArcBridgeService* arc_bridge_service)
: ArcAccessibilityHelperBridge(browser_context, arc_bridge_service),
window_(new aura::Window(nullptr)) {
window_->Init(ui::LAYER_NOT_DRAWN);
}
~TestArcAccessibilityHelperBridge() override { window_.reset(); }
void SetActiveWindowId(const std::string& id) {
exo::SetShellApplicationId(window_.get(), id);
}
protected:
aura::Window* GetActiveWindow() override { return window_.get(); }
private:
std::unique_ptr<aura::Window> window_;
DISALLOW_COPY_AND_ASSIGN(TestArcAccessibilityHelperBridge);
};
class ArcNotificationSurfaceManagerTest
: public ArcNotificationSurfaceManager {
public:
void AddObserver(Observer* observer) override {
observers_.AddObserver(observer);
};
void RemoveObserver(Observer* observer) override {
observers_.RemoveObserver(observer);
};
ArcNotificationSurface* GetArcSurface(
const std::string& notification_key) const override {
auto it = surfaces_.find(notification_key);
if (it == surfaces_.end())
return nullptr;
return it->second;
}
void AddSurface(ArcNotificationSurface* surface) {
surfaces_[surface->GetNotificationKey()] = surface;
for (auto& observer : observers_) {
observer.OnNotificationSurfaceAdded(surface);
}
}
void RemoveSurface(ArcNotificationSurface* surface) {
surfaces_.erase(surface->GetNotificationKey());
for (auto& observer : observers_) {
observer.OnNotificationSurfaceRemoved(surface);
}
}
private:
std::map<std::string, ArcNotificationSurface*> surfaces_;
base::ObserverList<Observer>::Unchecked observers_;
};
ArcAccessibilityHelperBridgeTest() = default;
void SetUp() override {
ChromeViewsTestBase::SetUp();
testing_profile_ = std::make_unique<TestingProfile>();
bridge_service_ = std::make_unique<ArcBridgeService>();
arc_notification_surface_manager_ =
std::make_unique<ArcNotificationSurfaceManagerTest>();
accessibility_helper_bridge_ =
std::make_unique<TestArcAccessibilityHelperBridge>(
testing_profile_.get(), bridge_service_.get());
}
void TearDown() override {
accessibility_helper_bridge_->Shutdown();
accessibility_helper_bridge_.reset();
arc_notification_surface_manager_.reset();
bridge_service_.reset();
testing_profile_.reset();
ChromeViewsTestBase::TearDown();
}
TestArcAccessibilityHelperBridge* accessibility_helper_bridge() {
return accessibility_helper_bridge_.get();
}
views::Widget* CreateTestWidget() {
views::Widget* widget = new views::Widget();
widget->Init(CreateParams(views::Widget::InitParams::TYPE_POPUP));
return widget;
}
views::View* GetContentsView(ArcNotificationView* notification_view) {
return notification_view->content_view_;
}
std::unique_ptr<message_center::Notification> CreateNotification() {
auto notification = std::make_unique<message_center::Notification>(
message_center::NOTIFICATION_TYPE_CUSTOM, kNotificationKey,
base::UTF8ToUTF16("title"), base::UTF8ToUTF16("message"), gfx::Image(),
base::UTF8ToUTF16("display_source"), GURL(),
message_center::NotifierId(
message_center::NotifierType::ARC_APPLICATION, "test_app_id"),
message_center::RichNotificationData(), nullptr);
notification->set_custom_view_type(ash::kArcNotificationCustomViewType);
return notification;
}
std::unique_ptr<ArcNotificationView> CreateArcNotificationView(
ArcNotificationItem* item,
const message_center::Notification& notification) {
return std::make_unique<ArcNotificationView>(item, notification);
}
protected:
std::unique_ptr<ArcNotificationSurfaceManagerTest>
arc_notification_surface_manager_;
private:
content::TestBrowserThreadBundle thread_bundle_;
std::unique_ptr<TestingProfile> testing_profile_;
std::unique_ptr<ArcBridgeService> bridge_service_;
std::unique_ptr<TestArcAccessibilityHelperBridge>
accessibility_helper_bridge_;
DISALLOW_COPY_AND_ASSIGN(ArcAccessibilityHelperBridgeTest);
};
TEST_F(ArcAccessibilityHelperBridgeTest, TaskAndAXTreeLifecycle) {
TestArcAccessibilityHelperBridge* helper_bridge =
accessibility_helper_bridge();
helper_bridge->set_filter_type_all_for_test();
const auto& task_id_to_tree = helper_bridge->task_id_to_tree_for_test();
ASSERT_EQ(0U, task_id_to_tree.size());
auto event1 = arc::mojom::AccessibilityEventData::New();
event1->source_id = 1;
event1->task_id = 1;
event1->event_type = arc::mojom::AccessibilityEventType::VIEW_FOCUSED;
event1->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
event1->node_data[0]->id = 1;
event1->node_data[0]->string_properties =
base::flat_map<arc::mojom::AccessibilityStringProperty, std::string>();
event1->node_data[0]->string_properties.value().insert(
std::make_pair(arc::mojom::AccessibilityStringProperty::PACKAGE_NAME,
"com.android.vending"));
// There's no active window.
helper_bridge->OnAccessibilityEvent(event1.Clone());
ASSERT_EQ(0U, task_id_to_tree.size());
// Let's make task 1 active by activating the window.
helper_bridge->SetActiveWindowId(std::string("org.chromium.arc.1"));
helper_bridge->OnAccessibilityEvent(event1.Clone());
ASSERT_EQ(1U, task_id_to_tree.size());
// Same package name, different task.
auto event2 = arc::mojom::AccessibilityEventData::New();
event2->source_id = 2;
event2->task_id = 2;
event2->event_type = arc::mojom::AccessibilityEventType::VIEW_FOCUSED;
event2->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
event2->node_data[0]->id = 2;
event2->node_data[0]->string_properties =
base::flat_map<arc::mojom::AccessibilityStringProperty, std::string>();
event2->node_data[0]->string_properties.value().insert(
std::make_pair(arc::mojom::AccessibilityStringProperty::PACKAGE_NAME,
"com.android.vending"));
// Active window is still task 1.
helper_bridge->OnAccessibilityEvent(event2.Clone());
ASSERT_EQ(1U, task_id_to_tree.size());
// Now make task 2 active.
helper_bridge->SetActiveWindowId(std::string("org.chromium.arc.2"));
helper_bridge->OnAccessibilityEvent(event2.Clone());
ASSERT_EQ(2U, task_id_to_tree.size());
// Same task id, different package name.
event2->node_data.clear();
event2->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
event2->source_id = 3;
event2->node_data[0]->id = 3;
event2->node_data[0]->string_properties =
base::flat_map<arc::mojom::AccessibilityStringProperty, std::string>();
event2->node_data[0]->string_properties.value().insert(
std::make_pair(arc::mojom::AccessibilityStringProperty::PACKAGE_NAME,
"com.google.music"));
// No new tasks tree mappings should have occurred.
helper_bridge->OnAccessibilityEvent(event2.Clone());
ASSERT_EQ(2U, task_id_to_tree.size());
helper_bridge->OnTaskDestroyed(1);
ASSERT_EQ(1U, task_id_to_tree.size());
helper_bridge->OnTaskDestroyed(2);
ASSERT_EQ(0U, task_id_to_tree.size());
}
// Accessibility event and surface creation/removal are sent in different
// channels, mojo and wayland. Order of those events can be changed. This is the
// case where mojo events arrive earlier than surface creation/removal.
//
// mojo: notification 1 created
// wayland: surface 1 added
// mojo: notification 1 removed
// mojo: notification 2 created
// wayland: surface 1 removed
// wayland: surface 2 added
// mojo: notification 2 removed
// wayland: surface 2 removed
TEST_F(ArcAccessibilityHelperBridgeTest, NotificationEventArriveFirst) {
TestArcAccessibilityHelperBridge* helper_bridge =
accessibility_helper_bridge();
arc_notification_surface_manager_->AddObserver(helper_bridge);
const auto& notification_key_to_tree_ =
helper_bridge->notification_key_to_tree_for_test();
ASSERT_EQ(0U, notification_key_to_tree_.size());
// mojo: notification 1 created
helper_bridge->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_CREATED);
auto event1 = arc::mojom::AccessibilityEventData::New();
event1->event_type = arc::mojom::AccessibilityEventType::WINDOW_STATE_CHANGED;
event1->notification_key = base::make_optional<std::string>(kNotificationKey);
event1->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
helper_bridge->OnAccessibilityEvent(event1.Clone());
EXPECT_EQ(1U, notification_key_to_tree_.size());
// wayland: surface 1 added
MockArcNotificationSurface test_surface(kNotificationKey);
arc_notification_surface_manager_->AddSurface(&test_surface);
// Confirm that axtree id is set to the surface.
auto it = notification_key_to_tree_.find(kNotificationKey);
EXPECT_NE(notification_key_to_tree_.end(), it);
AXTreeSourceArc* tree = it->second.get();
ui::AXTreeData tree_data;
tree->GetTreeData(&tree_data);
EXPECT_EQ(tree_data.tree_id, test_surface.GetAXTreeId());
// mojo: notification 1 removed
helper_bridge->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_REMOVED);
// Ax tree of the surface should be reset as the tree no longer exists.
EXPECT_EQ(ui::AXTreeIDUnknown(), test_surface.GetAXTreeId());
EXPECT_EQ(0U, notification_key_to_tree_.size());
// mojo: notification 2 created
helper_bridge->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_CREATED);
auto event3 = arc::mojom::AccessibilityEventData::New();
event3->event_type = arc::mojom::AccessibilityEventType::WINDOW_STATE_CHANGED;
event3->notification_key = base::make_optional<std::string>(kNotificationKey);
event3->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
helper_bridge->OnAccessibilityEvent(event3.Clone());
EXPECT_EQ(1U, notification_key_to_tree_.size());
// Ax tree from the second event is attached to the first surface. This is
// expected behavior.
auto it2 = notification_key_to_tree_.find(kNotificationKey);
EXPECT_NE(notification_key_to_tree_.end(), it2);
AXTreeSourceArc* tree2 = it2->second.get();
ui::AXTreeData tree_data2;
tree2->GetTreeData(&tree_data2);
EXPECT_EQ(tree_data2.tree_id, test_surface.GetAXTreeId());
// wayland: surface 1 removed
arc_notification_surface_manager_->RemoveSurface(&test_surface);
// Tree shouldn't be removed as a surface for the second one will come.
EXPECT_EQ(1U, notification_key_to_tree_.size());
// wayland: surface 2 added
MockArcNotificationSurface test_surface_2(kNotificationKey);
arc_notification_surface_manager_->AddSurface(&test_surface_2);
EXPECT_EQ(tree_data2.tree_id, test_surface_2.GetAXTreeId());
// mojo: notification 2 removed
helper_bridge->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_REMOVED);
EXPECT_EQ(0U, notification_key_to_tree_.size());
// wayland: surface 2 removed
arc_notification_surface_manager_->RemoveSurface(&test_surface_2);
}
// This is the case where surface creation/removal arrive before mojo events.
//
// wayland: surface 1 added
// wayland: surface 1 removed
// mojo: notification 1 created
// mojo: notification 1 removed
TEST_F(ArcAccessibilityHelperBridgeTest, NotificationSurfaceArriveFirst) {
TestArcAccessibilityHelperBridge* helper_bridge =
accessibility_helper_bridge();
arc_notification_surface_manager_->AddObserver(helper_bridge);
const auto& notification_key_to_tree_ =
helper_bridge->notification_key_to_tree_for_test();
ASSERT_EQ(0U, notification_key_to_tree_.size());
// wayland: surface 1 added
MockArcNotificationSurface test_surface(kNotificationKey);
arc_notification_surface_manager_->AddSurface(&test_surface);
// wayland: surface 1 removed
arc_notification_surface_manager_->RemoveSurface(&test_surface);
// mojo: notification 1 created
helper_bridge->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_CREATED);
auto event1 = arc::mojom::AccessibilityEventData::New();
event1->event_type = arc::mojom::AccessibilityEventType::WINDOW_STATE_CHANGED;
event1->notification_key = base::make_optional<std::string>(kNotificationKey);
event1->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
helper_bridge->OnAccessibilityEvent(event1.Clone());
EXPECT_EQ(1U, notification_key_to_tree_.size());
// mojo: notification 2 removed
helper_bridge->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_REMOVED);
EXPECT_EQ(0U, notification_key_to_tree_.size());
}
TEST_F(ArcAccessibilityHelperBridgeTest,
TextSelectionChangeActivateNotificationWidget) {
accessibility_helper_bridge()->set_filter_type_all_for_test();
// Prepare notification surface.
std::unique_ptr<MockArcNotificationSurface> surface =
std::make_unique<MockArcNotificationSurface>(kNotificationKey);
arc_notification_surface_manager_->AddSurface(surface.get());
// Prepare notification view with ArcNotificationContentView.
std::unique_ptr<MockArcNotificationItem> item =
std::make_unique<MockArcNotificationItem>(kNotificationKey);
std::unique_ptr<message_center::Notification> notification =
CreateNotification();
std::unique_ptr<ArcNotificationView> notification_view =
CreateArcNotificationView(item.get(), *notification.get());
notification_view->set_owned_by_client();
// Prepare widget to hold it.
views::Widget* widget = CreateTestWidget();
widget->widget_delegate()->set_can_activate(false);
widget->Deactivate();
widget->SetContentsView(notification_view.get());
widget->Show();
// Assert that the widget is not activatable.
ASSERT_FALSE(widget->CanActivate());
ASSERT_FALSE(widget->IsActive());
accessibility_helper_bridge()->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_CREATED);
// Dispatch text selection changed event.
auto event = arc::mojom::AccessibilityEventData::New();
event->event_type =
arc::mojom::AccessibilityEventType::VIEW_TEXT_SELECTION_CHANGED;
event->notification_key = base::make_optional<std::string>(kNotificationKey);
event->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
accessibility_helper_bridge()->OnAccessibilityEvent(event.Clone());
// Widget is activated.
EXPECT_TRUE(widget->CanActivate());
EXPECT_TRUE(widget->IsActive());
// Explicitly clear the focus to avoid ArcNotificationContentView::OnBlur is
// called which fails in this test set up.
widget->GetFocusManager()->ClearFocus();
// Widget needs to be closed before the test ends.
widget->Close();
// Remove surface cleanly before it's destructed.
arc_notification_surface_manager_->RemoveSurface(surface.get());
}
TEST_F(ArcAccessibilityHelperBridgeTest, TextSelectionChangedFocusContentView) {
accessibility_helper_bridge()->set_filter_type_all_for_test();
// Prepare notification surface.
std::unique_ptr<MockArcNotificationSurface> surface =
std::make_unique<MockArcNotificationSurface>(kNotificationKey);
arc_notification_surface_manager_->AddSurface(surface.get());
// Prepare notification view with ArcNotificationContentView.
std::unique_ptr<MockArcNotificationItem> item =
std::make_unique<MockArcNotificationItem>(kNotificationKey);
std::unique_ptr<message_center::Notification> notification =
CreateNotification();
std::unique_ptr<ArcNotificationView> notification_view =
CreateArcNotificationView(item.get(), *notification.get());
notification_view->set_owned_by_client();
// focus_stealer is a view which has initial focus.
std::unique_ptr<views::View> focus_stealer = std::make_unique<views::View>();
focus_stealer->set_owned_by_client();
// Prepare a widget to hold them.
views::Widget* widget = CreateTestWidget();
widget->GetRootView()->AddChildView(notification_view.get());
widget->GetRootView()->AddChildView(focus_stealer.get());
widget->Show();
// Put focus on focus_stealer.
focus_stealer->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
focus_stealer->RequestFocus();
// Assert that focus is on focus_stealer.
ASSERT_TRUE(widget->IsActive());
ASSERT_EQ(focus_stealer.get(), widget->GetFocusManager()->GetFocusedView());
accessibility_helper_bridge()->OnNotificationStateChanged(
kNotificationKey,
arc::mojom::AccessibilityNotificationStateType::SURFACE_CREATED);
// Dispatch text selection changed event.
auto event = arc::mojom::AccessibilityEventData::New();
event->event_type =
arc::mojom::AccessibilityEventType::VIEW_TEXT_SELECTION_CHANGED;
event->notification_key = base::make_optional<std::string>(kNotificationKey);
event->node_data.push_back(arc::mojom::AccessibilityNodeInfoData::New());
accessibility_helper_bridge()->OnAccessibilityEvent(event.Clone());
// Focus moves to contents view with text selection change.
EXPECT_EQ(GetContentsView(notification_view.get()),
widget->GetFocusManager()->GetFocusedView());
// Explicitly clear the focus to avoid ArcNotificationContentView::OnBlur is
// called which fails in this test set up.
widget->GetFocusManager()->ClearFocus();
// Widget needs to be closed before the test ends.
widget->Close();
// Remove surface cleanly before it's destructed.
arc_notification_surface_manager_->RemoveSurface(surface.get());
}
} // namespace arc