| // 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 <utility> |
| |
| #include "base/command_line.h" |
| #include "base/memory/singleton.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/app_list/arc/arc_app_list_prefs_factory.h" |
| #include "chromeos/chromeos_switches.h" |
| #include "components/arc/arc_bridge_service.h" |
| #include "components/arc/arc_browser_context_keyed_service_factory_base.h" |
| #include "components/arc/arc_service_manager.h" |
| #include "components/exo/shell_surface.h" |
| #include "components/exo/surface.h" |
| #include "components/exo/wm_helper.h" |
| #include "ui/arc/notification/arc_notification_surface.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/window.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/controls/native/native_view_host.h" |
| |
| namespace { |
| |
| constexpr int32_t kNoTaskId = -1; |
| |
| exo::Surface* GetArcSurface(const aura::Window* window) { |
| if (!window) |
| return nullptr; |
| |
| exo::Surface* arc_surface = exo::Surface::AsSurface(window); |
| if (!arc_surface) |
| arc_surface = exo::ShellSurface::GetMainSurface(window); |
| return arc_surface; |
| } |
| |
| int32_t GetTaskId(aura::Window* window) { |
| const std::string* arc_app_id = exo::ShellSurface::GetApplicationId(window); |
| if (!arc_app_id) |
| return kNoTaskId; |
| |
| int32_t task_id = kNoTaskId; |
| if (sscanf(arc_app_id->c_str(), "org.chromium.arc.%d", &task_id) != 1) |
| return kNoTaskId; |
| |
| return task_id; |
| } |
| |
| void DispatchFocusChange(arc::mojom::AccessibilityNodeInfoData* node_data, |
| Profile* profile) { |
| chromeos::AccessibilityManager* accessibility_manager = |
| chromeos::AccessibilityManager::Get(); |
| if (!accessibility_manager || accessibility_manager->profile() != profile) |
| return; |
| |
| exo::WMHelper* wm_helper = exo::WMHelper::GetInstance(); |
| if (!wm_helper) |
| return; |
| |
| aura::Window* focused_window = wm_helper->GetFocusedWindow(); |
| if (!focused_window) |
| return; |
| |
| aura::Window* toplevel_window = focused_window->GetToplevelWindow(); |
| |
| gfx::Rect bounds_in_screen = gfx::ScaleToEnclosingRect( |
| node_data->bounds_in_screen, |
| 1.0f / toplevel_window->layer()->device_scale_factor()); |
| |
| accessibility_manager->OnViewFocusedInArc(bounds_in_screen); |
| } |
| |
| arc::mojom::AccessibilityFilterType GetFilterTypeForProfile(Profile* profile) { |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| chromeos::switches::kEnableChromeVoxArcSupport)) { |
| return arc::mojom::AccessibilityFilterType::ALL; |
| } |
| |
| chromeos::AccessibilityManager* accessibility_manager = |
| chromeos::AccessibilityManager::Get(); |
| if (!accessibility_manager) |
| return arc::mojom::AccessibilityFilterType::OFF; |
| |
| // TODO(yawano): Support the case where primary user is in background. |
| if (accessibility_manager->profile() != profile) |
| return arc::mojom::AccessibilityFilterType::OFF; |
| |
| if (accessibility_manager->IsSelectToSpeakEnabled() || |
| accessibility_manager->IsSwitchAccessEnabled()) { |
| return arc::mojom::AccessibilityFilterType::ALL; |
| } |
| |
| if (accessibility_manager->IsSpokenFeedbackEnabled()) |
| return arc::mojom::AccessibilityFilterType::WHITELISTED_PACKAGE_NAME; |
| |
| if (accessibility_manager->IsFocusHighlightEnabled()) |
| return arc::mojom::AccessibilityFilterType::FOCUS; |
| |
| return arc::mojom::AccessibilityFilterType::OFF; |
| } |
| |
| } // namespace |
| |
| namespace arc { |
| |
| namespace { |
| |
| // Singleton factory for ArcAccessibilityHelperBridge. |
| class ArcAccessibilityHelperBridgeFactory |
| : public internal::ArcBrowserContextKeyedServiceFactoryBase< |
| ArcAccessibilityHelperBridge, |
| ArcAccessibilityHelperBridgeFactory> { |
| public: |
| // Factory name used by ArcBrowserContextKeyedServiceFactoryBase. |
| static constexpr const char* kName = "ArcAccessibilityHelperBridgeFactory"; |
| |
| static ArcAccessibilityHelperBridgeFactory* GetInstance() { |
| return base::Singleton<ArcAccessibilityHelperBridgeFactory>::get(); |
| } |
| |
| private: |
| friend struct base::DefaultSingletonTraits< |
| ArcAccessibilityHelperBridgeFactory>; |
| |
| ArcAccessibilityHelperBridgeFactory() { |
| // ArcAccessibilityHelperBridge needs to track task creation and |
| // destruction in the container, which are notified to ArcAppListPrefs |
| // via Mojo. |
| DependsOn(ArcAppListPrefsFactory::GetInstance()); |
| } |
| ~ArcAccessibilityHelperBridgeFactory() override = default; |
| }; |
| |
| } // namespace |
| |
| ArcAccessibilityHelperBridge::CountedAXTree::CountedAXTree( |
| AXTreeSourceArc* ax_tree) |
| : count(1U) { |
| tree.reset(ax_tree); |
| } |
| |
| ArcAccessibilityHelperBridge::CountedAXTree::~CountedAXTree() {} |
| |
| // static |
| ArcAccessibilityHelperBridge* |
| ArcAccessibilityHelperBridge::GetForBrowserContext( |
| content::BrowserContext* context) { |
| return ArcAccessibilityHelperBridgeFactory::GetForBrowserContext(context); |
| } |
| |
| ArcAccessibilityHelperBridge::ArcAccessibilityHelperBridge( |
| content::BrowserContext* browser_context, |
| ArcBridgeService* arc_bridge_service) |
| : profile_(Profile::FromBrowserContext(browser_context)), |
| arc_bridge_service_(arc_bridge_service) { |
| arc_bridge_service_->accessibility_helper()->SetHost(this); |
| arc_bridge_service_->accessibility_helper()->AddObserver(this); |
| |
| // Null on testing. |
| auto* app_list_prefs = ArcAppListPrefs::Get(profile_); |
| if (app_list_prefs) |
| app_list_prefs->AddObserver(this); |
| } |
| |
| ArcAccessibilityHelperBridge::~ArcAccessibilityHelperBridge() = default; |
| |
| void ArcAccessibilityHelperBridge::SetNativeChromeVoxArcSupport(bool enabled) { |
| aura::Window* window = GetActiveWindow(); |
| if (!window) |
| return; |
| int32_t task_id = GetTaskId(window); |
| if (task_id == kNoTaskId) |
| return; |
| |
| auto* instance = |
| ARC_GET_INSTANCE_FOR_METHOD(arc_bridge_service_->accessibility_helper(), |
| SetNativeChromeVoxArcSupportForFocusedWindow); |
| instance->SetNativeChromeVoxArcSupportForFocusedWindow( |
| enabled, base::Bind(&ArcAccessibilityHelperBridge:: |
| OnSetNativeChromeVoxArcSupportProcessed, |
| base::Unretained(this), enabled)); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnSetNativeChromeVoxArcSupportProcessed( |
| bool enabled, |
| bool processed) { |
| if (!enabled) |
| task_id_to_tree_.clear(); |
| } |
| |
| void ArcAccessibilityHelperBridge::Shutdown() { |
| // We do not unregister ourselves from WMHelper as an ActivationObserver |
| // because it is always null at this point during teardown. |
| |
| // Null on testing. |
| auto* app_list_prefs = ArcAppListPrefs::Get(profile_); |
| if (app_list_prefs) |
| app_list_prefs->RemoveObserver(this); |
| |
| arc_bridge_service_->accessibility_helper()->RemoveObserver(this); |
| arc_bridge_service_->accessibility_helper()->SetHost(nullptr); |
| |
| auto* surface_manager = ArcNotificationSurfaceManager::Get(); |
| if (surface_manager) |
| surface_manager->RemoveObserver(this); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnConnectionReady() { |
| UpdateFilterType(); |
| |
| chromeos::AccessibilityManager* accessibility_manager = |
| chromeos::AccessibilityManager::Get(); |
| if (accessibility_manager) { |
| accessibility_status_subscription_ = |
| accessibility_manager->RegisterCallback(base::BindRepeating( |
| &ArcAccessibilityHelperBridge::OnAccessibilityStatusChanged, |
| base::Unretained(this))); |
| } |
| |
| auto* surface_manager = ArcNotificationSurfaceManager::Get(); |
| if (surface_manager) |
| surface_manager->AddObserver(this); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnAccessibilityEventDeprecated( |
| mojom::AccessibilityEventType event_type, |
| mojom::AccessibilityNodeInfoDataPtr event_source) { |
| if (event_type == arc::mojom::AccessibilityEventType::VIEW_FOCUSED) |
| DispatchFocusChange(event_source.get(), profile_); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnAccessibilityEvent( |
| mojom::AccessibilityEventDataPtr event_data) { |
| // TODO(yawano): Handle AccessibilityFilterType::OFF. |
| arc::mojom::AccessibilityFilterType filter_type = |
| GetFilterTypeForProfile(profile_); |
| |
| if (filter_type == arc::mojom::AccessibilityFilterType::ALL || |
| filter_type == |
| arc::mojom::AccessibilityFilterType::WHITELISTED_PACKAGE_NAME) { |
| if (event_data->node_data.empty()) |
| return; |
| |
| AXTreeSourceArc* tree_source = nullptr; |
| bool is_notification_event = event_data->notification_key.has_value(); |
| if (is_notification_event) { |
| std::string notification_key = event_data->notification_key.value(); |
| bool increment_counter = |
| event_data->event_type == |
| arc::mojom::AccessibilityEventType::WINDOW_STATE_CHANGED; |
| tree_source = |
| GetOrCreateFromNotificationKey(notification_key, increment_counter); |
| } else { |
| if (event_data->task_id == kNoTaskId) |
| return; |
| |
| aura::Window* active_window = GetActiveWindow(); |
| if (!active_window) |
| return; |
| |
| int32_t task_id = GetTaskId(active_window); |
| if (task_id != event_data->task_id) |
| return; |
| |
| tree_source = GetOrCreateFromTaskId(event_data->task_id); |
| tree_source->Focus(active_window); |
| } |
| |
| if (!tree_source) |
| return; |
| |
| tree_source->NotifyAccessibilityEvent(event_data.get()); |
| |
| auto* surface_manager = ArcNotificationSurfaceManager::Get(); |
| if (surface_manager && is_notification_event && |
| event_data->event_type == |
| arc::mojom::AccessibilityEventType::WINDOW_STATE_CHANGED) { |
| std::string notification_key = event_data->notification_key.value(); |
| ArcNotificationSurface* surface = |
| surface_manager->GetArcSurface(notification_key); |
| |
| ui::AXTreeData tree_data; |
| if (surface && tree_source->GetTreeData(&tree_data)) { |
| surface->SetAXTreeId(tree_data.tree_id); |
| |
| // Dispatch AX_EVENT_CHILDREN_CHANGED to force AXNodeData of the |
| // notification updated. Before AXTreeId is set, its AXNodeData is |
| // populated as a button. |
| if (surface->IsAttached()) { |
| surface->GetAttachedHost()->NotifyAccessibilityEvent( |
| ui::AX_EVENT_CHILDREN_CHANGED, false); |
| } |
| } |
| } |
| |
| return; |
| } |
| |
| if (event_data->event_type != |
| arc::mojom::AccessibilityEventType::VIEW_FOCUSED) |
| return; |
| |
| CHECK_EQ(1U, event_data.get()->node_data.size()); |
| DispatchFocusChange(event_data.get()->node_data[0].get(), profile_); |
| } |
| |
| AXTreeSourceArc* ArcAccessibilityHelperBridge::GetOrCreateFromTaskId( |
| int32_t task_id) { |
| AXTreeSourceArc* tree_source = nullptr; |
| auto tree_it = task_id_to_tree_.find(task_id); |
| if (tree_it == task_id_to_tree_.end()) { |
| task_id_to_tree_[task_id].reset(new AXTreeSourceArc(this)); |
| tree_source = task_id_to_tree_[task_id].get(); |
| } else { |
| tree_source = tree_it->second.get(); |
| } |
| return tree_source; |
| } |
| |
| AXTreeSourceArc* ArcAccessibilityHelperBridge::GetOrCreateFromNotificationKey( |
| const std::string& notification_key, |
| bool increment_counter) { |
| auto tree_it = notification_key_to_tree_.find(notification_key); |
| if (tree_it == notification_key_to_tree_.end()) { |
| notification_key_to_tree_[notification_key].reset( |
| new CountedAXTree(new AXTreeSourceArc(this))); |
| return notification_key_to_tree_[notification_key]->tree.get(); |
| } |
| |
| if (increment_counter) |
| tree_it->second->count++; |
| |
| return tree_it->second->tree.get(); |
| } |
| |
| AXTreeSourceArc* ArcAccessibilityHelperBridge::GetFromTreeId( |
| int32_t tree_id) const { |
| for (auto it = task_id_to_tree_.begin(); it != task_id_to_tree_.end(); ++it) { |
| ui::AXTreeData tree_data; |
| it->second->GetTreeData(&tree_data); |
| if (tree_data.tree_id == tree_id) |
| return it->second.get(); |
| } |
| |
| for (auto notification_it = notification_key_to_tree_.begin(); |
| notification_it != notification_key_to_tree_.end(); ++notification_it) { |
| ui::AXTreeData tree_data; |
| notification_it->second->tree->GetTreeData(&tree_data); |
| if (tree_data.tree_id == tree_id) |
| return notification_it->second->tree.get(); |
| } |
| |
| return nullptr; |
| } |
| |
| void ArcAccessibilityHelperBridge::OnAction( |
| const ui::AXActionData& data) const { |
| arc::mojom::AccessibilityActionDataPtr action_data = |
| arc::mojom::AccessibilityActionData::New(); |
| |
| action_data->node_id = data.target_node_id; |
| |
| AXTreeSourceArc* tree_source = GetFromTreeId(data.target_tree_id); |
| if (!tree_source) |
| return; |
| action_data->window_id = tree_source->window_id(); |
| |
| switch (data.action) { |
| case ui::AX_ACTION_DO_DEFAULT: |
| action_data->action_type = arc::mojom::AccessibilityActionType::CLICK; |
| break; |
| case ui::AX_ACTION_FOCUS: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS; |
| break; |
| case ui::AX_ACTION_SCROLL_TO_MAKE_VISIBLE: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::SHOW_ON_SCREEN; |
| break; |
| case ui::AX_ACTION_SCROLL_BACKWARD: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::SCROLL_BACKWARD; |
| break; |
| case ui::AX_ACTION_SCROLL_FORWARD: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::SCROLL_FORWARD; |
| break; |
| case ui::AX_ACTION_SCROLL_UP: |
| action_data->action_type = arc::mojom::AccessibilityActionType::SCROLL_UP; |
| break; |
| case ui::AX_ACTION_SCROLL_DOWN: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::SCROLL_DOWN; |
| break; |
| case ui::AX_ACTION_SCROLL_LEFT: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::SCROLL_LEFT; |
| break; |
| case ui::AX_ACTION_SCROLL_RIGHT: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::SCROLL_RIGHT; |
| break; |
| case ui::AX_ACTION_CUSTOM_ACTION: |
| action_data->action_type = |
| arc::mojom::AccessibilityActionType::CUSTOM_ACTION; |
| action_data->custom_action_id = data.custom_action_id; |
| break; |
| default: |
| return; |
| } |
| |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_bridge_service_->accessibility_helper(), PerformAction); |
| instance->PerformAction( |
| std::move(action_data), |
| base::Bind(&ArcAccessibilityHelperBridge::OnActionResult, |
| base::Unretained(this), data)); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnActionResult(const ui::AXActionData& data, |
| bool result) const { |
| AXTreeSourceArc* tree_source = GetFromTreeId(data.target_tree_id); |
| |
| if (!tree_source) |
| return; |
| |
| tree_source->NotifyActionResult(data, result); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnAccessibilityStatusChanged( |
| const chromeos::AccessibilityStatusEventDetails& event_details) { |
| // TODO(yawano): Add case for select to speak and switch access. |
| if (event_details.notification_type != |
| chromeos::ACCESSIBILITY_TOGGLE_SPOKEN_FEEDBACK && |
| event_details.notification_type != |
| chromeos::ACCESSIBILITY_TOGGLE_FOCUS_HIGHLIGHT) { |
| return; |
| } |
| |
| UpdateFilterType(); |
| UpdateTouchExplorationPassThrough(GetActiveWindow()); |
| } |
| |
| void ArcAccessibilityHelperBridge::UpdateFilterType() { |
| arc::mojom::AccessibilityFilterType filter_type = |
| GetFilterTypeForProfile(profile_); |
| |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_bridge_service_->accessibility_helper(), SetFilter); |
| if (instance) |
| instance->SetFilter(filter_type); |
| |
| bool add_activation_observer = |
| filter_type == arc::mojom::AccessibilityFilterType::ALL || |
| filter_type == |
| arc::mojom::AccessibilityFilterType::WHITELISTED_PACKAGE_NAME; |
| if (add_activation_observer == activation_observer_added_) |
| return; |
| |
| exo::WMHelper* wm_helper = exo::WMHelper::GetInstance(); |
| if (!wm_helper) |
| return; |
| |
| if (add_activation_observer) |
| wm_helper->AddActivationObserver(this); |
| else |
| wm_helper->RemoveActivationObserver(this); |
| } |
| |
| void ArcAccessibilityHelperBridge::UpdateTouchExplorationPassThrough( |
| aura::Window* window) { |
| if (!window) |
| return; |
| |
| if (!GetArcSurface(window)) |
| return; |
| |
| // First, do a lookup for the task id associated with this app. There should |
| // always be a valid entry. |
| int32_t task_id = GetTaskId(window); |
| |
| // Do a lookup for the tree source. A tree source may not exist because the |
| // app isn't whitelisted Android side or no data has been received for the |
| // app. |
| auto it = task_id_to_tree_.find(task_id); |
| window->SetProperty(aura::client::kAccessibilityTouchExplorationPassThrough, |
| it == task_id_to_tree_.end()); |
| } |
| |
| aura::Window* ArcAccessibilityHelperBridge::GetActiveWindow() { |
| exo::WMHelper* wm_helper = exo::WMHelper::GetInstance(); |
| if (!wm_helper) |
| return nullptr; |
| |
| return wm_helper->GetActiveWindow(); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnWindowActivated( |
| ActivationReason reason, |
| aura::Window* gained_active, |
| aura::Window* lost_active) { |
| if (gained_active == lost_active) |
| return; |
| |
| UpdateTouchExplorationPassThrough(gained_active); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnTaskDestroyed(int32_t task_id) { |
| task_id_to_tree_.erase(task_id); |
| } |
| |
| void ArcAccessibilityHelperBridge::OnNotificationSurfaceAdded( |
| ArcNotificationSurface* surface) { |
| const std::string& notification_key = surface->GetNotificationKey(); |
| |
| AXTreeSourceArc* tree = GetOrCreateFromNotificationKey( |
| notification_key, false /* increment_counter */); |
| if (!tree) |
| return; |
| |
| ui::AXTreeData tree_data; |
| if (!tree->GetTreeData(&tree_data)) |
| return; |
| |
| surface->SetAXTreeId(tree_data.tree_id); |
| |
| // Dispatch AX_EVENT_CHILDREN_CHANGED to force AXNodeData of the notification |
| // updated. As order of OnNotificationSurfaceAdded call is not guaranteed, we |
| // are dispatching the event in both ArcAccessibilityHelperBridge and |
| // ArcNotificationContentView. The event needs to be dispatched after 1. ax |
| // tree id is set to the surface, 2 the surface is attached to the content |
| // view. |
| if (surface->IsAttached()) { |
| surface->GetAttachedHost()->NotifyAccessibilityEvent( |
| ui::AX_EVENT_CHILDREN_CHANGED, false); |
| } |
| } |
| |
| void ArcAccessibilityHelperBridge::OnNotificationSurfaceRemoved( |
| ArcNotificationSurface* surface) { |
| const std::string& notification_key = surface->GetNotificationKey(); |
| auto it = notification_key_to_tree_.find(notification_key); |
| if (it == notification_key_to_tree_.end()) |
| return; |
| |
| it->second->count--; |
| |
| CHECK(it->second->count >= 0); |
| |
| if (it->second->count == 0) |
| notification_key_to_tree_.erase(notification_key); |
| } |
| |
| } // namespace arc |