/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho;
import java.util.List;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
/**
* Class that is used to set up accessibility for {@link ComponentHost}s.
* Virtual nodes are only exposed if the component implements support for
* extra accessibility nodes.
*/
class ComponentAccessibilityDelegate extends ExploreByTouchHelper {
private static final String TAG = "ComponentAccessibility";
private final View mView;
private NodeInfo mNodeInfo;
private final AccessibilityDelegateCompat mSuperDelegate;
private static Rect sDefaultBounds;
ComponentAccessibilityDelegate(View view, NodeInfo nodeInfo) {
super(view);
mView = view;
mNodeInfo = nodeInfo;
mSuperDelegate = new SuperDelegate();
}
ComponentAccessibilityDelegate(View view) {
this(view, null);
}
/**
* {@link ComponentHost} contains the logic for setting the {@link NodeInfo} containing the
* {@link EventHandler}s for its delegate instance whenever it is set/unset
*
* @see ComponentHost#setTag(int, Object)
*/
void setNodeInfo(NodeInfo nodeInfo) {
mNodeInfo = nodeInfo;
}
@Override
public void onInitializeAccessibilityNodeInfo(
View host,
AccessibilityNodeInfoCompat node) {
final MountItem mountItem = getAccessibleMountItem(mView);
if (
mNodeInfo != null
&& mNodeInfo.getOnInitializeAccessibilityNodeInfoHandler() != null) {
EventDispatcherUtils.dispatchOnInitializeAccessibilityNodeInfoEvent(
mNodeInfo.getOnInitializeAccessibilityNodeInfoHandler(),
host,
node,
mSuperDelegate);
} else if (mountItem != null) {
super.onInitializeAccessibilityNodeInfo(host, node);
// Coalesce the accessible mount item's information with the
// the root host view's as they are meant to behave as a single
// node in the accessibility framework.
final Component<?> component = mountItem.getComponent();
component.getLifecycle().onPopulateAccessibilityNode(node, component);
} else {
super.onInitializeAccessibilityNodeInfo(host, node);
}
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
final MountItem mountItem = getAccessibleMountItem(mView);
if (mountItem == null) {
return;
}
final Component<?> component = mountItem.getComponent();
final int extraAccessibilityNodesCount =
component.getLifecycle().getExtraAccessibilityNodesCount(component);
// Expose extra accessibility nodes declared by the component to the
// accessibility framework. The actual nodes will be populated in
// {@link #onPopulateNodeForVirtualView}.
for (int i = 0; i < extraAccessibilityNodesCount; i++) {
virtualViewIds.add(i);
}
}
@Override
protected void onPopulateNodeForVirtualView(
int virtualViewId,
AccessibilityNodeInfoCompat node) {
final MountItem mountItem = getAccessibleMountItem(mView);
if (mountItem == null) {
Log.e(TAG, "No accessible mount item found for view: " + mView);
// ExploreByTouchHelper insists that we set something.
node.setContentDescription("");
node.setBoundsInParent(getDefaultBounds());
return;
}
final Drawable drawable = (Drawable) mountItem.getContent();
final Rect bounds = drawable.getBounds();
final Component<?> component = mountItem.getComponent();
final ComponentLifecycle lifecycle = component.getLifecycle();
node.setClassName(lifecycle.getClass().getName());
if (virtualViewId >= lifecycle.getExtraAccessibilityNodesCount(component)) {
Log.e(TAG, "Received unrecognized virtual view id: " + virtualViewId);
// ExploreByTouchHelper insists that we set something.
node.setContentDescription("");
node.setBoundsInParent(getDefaultBounds());
return;
}
lifecycle.onPopulateExtraAccessibilityNode(
node,
virtualViewId,
bounds.left,
bounds.top,
component);
}
/**
* Finds extra accessibility nodes under the given event coordinates.
* Returns {@link #INVALID_ID} otherwise.
*/
@Override
protected int getVirtualViewAt(float x, float y) {
final MountItem mountItem = getAccessibleMountItem(mView);
if (mountItem == null) {
return INVALID_ID;
}
final Component<?> component = mountItem.getComponent();
final ComponentLifecycle lifecycle = component.getLifecycle();
if (lifecycle.getExtraAccessibilityNodesCount(component) == 0) {
return INVALID_ID;
}
final Drawable drawable = (Drawable) mountItem.getContent();
final Rect bounds = drawable.getBounds();
// Try to find an extra accessibility node that intersects with
// the given coordinates.
final int virtualViewId = lifecycle.getExtraAccessibilityNodeAt(
(int) x - bounds.left,
(int) y - bounds.top,
component);
return (virtualViewId >= 0 ? virtualViewId : INVALID_ID);
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
// TODO (T10543861): ExploreByTouchHelper enforces subclasses to set a content description
// or text on new events but components don't provide APIs to do so yet.
event.setContentDescription("");
}
@Override
protected boolean onPerformActionForVirtualView(
int virtualViewId,
int action,
Bundle arguments) {
return false;
}
/**
* Returns a {AccessibilityNodeProviderCompat} if the host contains a component
* that implements custom accessibility logic. Returns {@code NULL} otherwise.
* Components with accessibility content are automatically wrapped in hosts by
* {@link LayoutState}.
*/
@Override
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
final MountItem mountItem = getAccessibleMountItem(mView);
if (mountItem != null
&& mountItem.getComponent().getLifecycle().implementsExtraAccessibilityNodes()) {
return super.getAccessibilityNodeProvider(host);
}
return null;
}
private static MountItem getAccessibleMountItem(View view) {
if (!(view instanceof ComponentHost)) {
return null;
}
return ((ComponentHost) view).getAccessibleMountItem();
}
@Override
public void onInitializeAccessibilityEvent(
View host, AccessibilityEvent event) {
if (mNodeInfo != null && mNodeInfo.getOnInitializeAccessibilityEventHandler() != null) {
EventDispatcherUtils.dispatchOnInitializeAccessibilityEvent(
mNodeInfo.getOnInitializeAccessibilityEventHandler(),
host,
event,
mSuperDelegate);
} else {
super.onInitializeAccessibilityEvent(host, event);
}
}
@Override
public void sendAccessibilityEvent(View host, int eventType) {
if (mNodeInfo != null && mNodeInfo.getSendAccessibilityEventHandler() != null) {
EventDispatcherUtils.dispatchSendAccessibilityEvent(
mNodeInfo.getSendAccessibilityEventHandler(),
host,
eventType,
mSuperDelegate);
} else {
super.sendAccessibilityEvent(host, eventType);
}
}
@Override
public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
if (mNodeInfo != null && mNodeInfo.getSendAccessibilityEventUncheckedHandler() != null) {
EventDispatcherUtils.dispatchSendAccessibilityEventUnchecked(
mNodeInfo.getSendAccessibilityEventUncheckedHandler(),
host,
event,
mSuperDelegate);
} else {
super.sendAccessibilityEventUnchecked(host, event);
}
}
@Override
public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
if (mNodeInfo != null && mNodeInfo.getDispatchPopulateAccessibilityEventHandler() != null ) {
return EventDispatcherUtils.dispatchDispatchPopulateAccessibilityEvent(
mNodeInfo.getDispatchPopulateAccessibilityEventHandler(),
host,
event,
mSuperDelegate);
}
return super.dispatchPopulateAccessibilityEvent(host, event);
}
@Override
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
if (mNodeInfo != null && mNodeInfo.getOnPopulateAccessibilityEventHandler() != null) {
EventDispatcherUtils.dispatchOnPopulateAccessibilityEvent(
mNodeInfo.getOnPopulateAccessibilityEventHandler(),
host,
event,
mSuperDelegate);
} else {
super.onPopulateAccessibilityEvent(host, event);
}
}
@Override
public boolean onRequestSendAccessibilityEvent(
ViewGroup host,
View child,
AccessibilityEvent event) {
if (mNodeInfo != null && mNodeInfo.getOnRequestSendAccessibilityEventHandler() != null) {
return EventDispatcherUtils.dispatchOnRequestSendAccessibilityEvent(
mNodeInfo.getOnRequestSendAccessibilityEventHandler(),
host,
child,
event,
mSuperDelegate);
}
return super.onRequestSendAccessibilityEvent(host, child, event);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (mNodeInfo != null && mNodeInfo.getPerformAccessibilityActionHandler() != null) {
return EventDispatcherUtils.dispatchPerformAccessibilityActionEvent(
mNodeInfo.getPerformAccessibilityActionHandler(),
host,
action,
args,
mSuperDelegate);
}
return super.performAccessibilityAction(host, action, args);
}
private static Rect getDefaultBounds() {
synchronized(sDefaultBounds) {
if (sDefaultBounds == null) {
sDefaultBounds = new Rect(0, 0, 1, 1);
}
}
return sDefaultBounds;
}
private class SuperDelegate extends AccessibilityDelegateCompat {
@Override
public boolean dispatchPopulateAccessibilityEvent(
View host, AccessibilityEvent event) {
return ComponentAccessibilityDelegate.super.dispatchPopulateAccessibilityEvent(host, event);
}
@Override
public void onInitializeAccessibilityEvent(
View host, AccessibilityEvent event) {
ComponentAccessibilityDelegate.super.onInitializeAccessibilityEvent(host, event);
}
@Override
public void onInitializeAccessibilityNodeInfo(
View host, AccessibilityNodeInfoCompat node) {
ComponentAccessibilityDelegate.super.onInitializeAccessibilityNodeInfo(host, node);
}
@Override
public void onPopulateAccessibilityEvent(
View host, AccessibilityEvent event) {
ComponentAccessibilityDelegate.super.onPopulateAccessibilityEvent(host, event);
}
@Override
public boolean onRequestSendAccessibilityEvent(
ViewGroup host, View child, AccessibilityEvent event) {
return ComponentAccessibilityDelegate.super.onRequestSendAccessibilityEvent(
host,
child,
event);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
return ComponentAccessibilityDelegate.super.performAccessibilityAction(host, action, args);
}
@Override
public void sendAccessibilityEvent(View host, int eventType) {
ComponentAccessibilityDelegate.super.sendAccessibilityEvent(host, eventType);
}
@Override
public void sendAccessibilityEventUnchecked(
View host, AccessibilityEvent event) {
ComponentAccessibilityDelegate.super.sendAccessibilityEventUnchecked(host, event);
}
}
}