Bug 1330257 - 1. Refactor SessionAccessibility; r=eeejay

Move the AccessibilityNodeProvider implementation under
SessionAccessibility, to reduce the indent of the code.

Also make all methods in SessionAccessibility.Settings static to make
the code easier to follow.

Differential Revision: https://phabricator.services.mozilla.com/D3250
This commit is contained in:
Jim Chen 2018-08-20 22:28:19 -04:00
parent 28e9ca261d
commit 6b8899beef

View File

@ -29,18 +29,177 @@ import android.view.accessibility.AccessibilityNodeProvider;
public class SessionAccessibility {
private static final String LOGTAG = "GeckoAccessibility";
// This is a special ID we use for nodes that are eent sources.
// This is a special ID we use for nodes that are event sources.
// We expose it as a fragment and not an actual child of the View node.
private static final int VIRTUAL_CONTENT_ID = -2;
// This is the number BrailleBack uses to start indexing routing keys.
private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
/* package */ final class NodeProvider extends AccessibilityNodeProvider {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null) ?
AccessibilityNodeInfo.obtain(mVirtualContentNode) :
AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
switch (virtualDescendantId) {
case View.NO_ID:
// This is the parent View node.
// We intentionally don't add VIRTUAL_CONTENT_ID
// as a child. It is a source for events,
// but not a member of the tree you
// can get to by traversing down.
if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
// When running junit tests we don't have a display
mView.onInitializeAccessibilityNodeInfo(info);
}
info.setClassName("android.webkit.WebView"); // TODO: WTF
if (Build.VERSION.SDK_INT >= 19) {
Bundle bundle = info.getExtras();
bundle.putCharSequence(
"ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
"ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
"FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
"HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
"MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
"UNVISITED_LINK,VISITED_LINK");
}
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addChild(hostView, VIRTUAL_CONTENT_ID);
break;
default:
info.setParent(mView);
info.setSource(mView, virtualDescendantId);
info.setVisibleToUser(mView.isShown());
info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
info.setEnabled(true);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
break;
}
return info;
}
@Override
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
if (virtualViewId == View.NO_ID) {
return performRootAction(action, arguments);
}
return performContentAction(action, arguments);
}
private boolean performRootAction(int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
final GeckoBundle data = new GeckoBundle(1);
data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
return true;
}
return mView.performAccessibilityAction(action, arguments);
}
@SuppressWarnings("fallthrough")
private boolean performContentAction(int action, Bundle arguments) {
final GeckoBundle data;
switch (action) {
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
return true;
case AccessibilityNodeInfo.ACTION_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
return true;
case AccessibilityNodeInfo.ACTION_LONG_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
return true;
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
return true;
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
return true;
case AccessibilityNodeInfo.ACTION_SELECT:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySelect", null);
return true;
case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
if (mLastItem) {
return false;
}
// fall-through
case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
if (arguments != null) {
data = new GeckoBundle(1);
data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
} else {
data = null;
}
mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
"GeckoView:AccessibilityNext" : "GeckoView:AccessibilityPrevious", data);
return true;
case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
// XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
// the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
// Other negative values are used by ChromeVox, but we don't support them.
// FAKE_GRANULARITY_READ_CURRENT = -1
// FAKE_GRANULARITY_READ_TITLE = -2
// FAKE_GRANULARITY_STOP_SPEECH = -3
// FAKE_GRANULARITY_CHANGE_SHIFTER = -4
int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
data = new GeckoBundle(1);
data.putInt("keyIndex", keyIndex);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
} else if (granularity > 0) {
boolean extendSelection = arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
data = new GeckoBundle(3);
data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
data.putInt("granularity", granularity);
data.putBoolean("select", extendSelection);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
}
return true;
case AccessibilityNodeInfo.ACTION_SET_SELECTION:
if (arguments == null) {
return false;
}
int selectionStart = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);
int selectionEnd = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT);
data = new GeckoBundle(2);
data.putInt("start", selectionStart);
data.putInt("end", selectionEnd);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySetSelection", data);
return true;
case AccessibilityNodeInfo.ACTION_CUT:
case AccessibilityNodeInfo.ACTION_COPY:
case AccessibilityNodeInfo.ACTION_PASTE:
data = new GeckoBundle(1);
data.putInt("action", action);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityClipboard", data);
return true;
}
return mView.performAccessibilityAction(action, arguments);
}
};
// Gecko session we are proxying
/* package */ final GeckoSession mSession;
// This is the view that delegates accessibility to us. We also sends event through it.
private View mView;
// Aave we reached the last item in content?
// Have we reached the last item in content?
private boolean mLastItem;
// Used to store the JSON message and populate the event later in the code path.
private AccessibilityNodeInfo mVirtualContentNode;
@ -48,7 +207,7 @@ public class SessionAccessibility {
/* package */ SessionAccessibility(final GeckoSession session) {
mSession = session;
Settings.getInstance().dispatch();
Settings.updateAccessibilitySettings();
session.getEventDispatcher().registerUiThreadListener(new BundleEventListener() {
@Override
@ -86,202 +245,32 @@ public class SessionAccessibility {
}
mView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
private AccessibilityNodeProvider mAccessibilityNodeProvider;
private NodeProvider mProvider;
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider(final View hostView) {
if (mAccessibilityNodeProvider == null)
mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
assertAttachedView(hostView);
AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null) ?
AccessibilityNodeInfo.obtain(mVirtualContentNode) :
AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
switch (virtualDescendantId) {
case View.NO_ID:
// This is the parent View node.
// We intentionally don't add VIRTUAL_CONTENT_ID
// as a child. It is a source for events,
// but not a member of the tree you
// can get to by traversing down.
if (mView.getDisplay() != null) {
// When running junit tests we don't have a display
onInitializeAccessibilityNodeInfo(mView, info);
}
info.setClassName("android.webkit.WebView"); // TODO: WTF
if (Build.VERSION.SDK_INT >= 19) {
Bundle bundle = info.getExtras();
bundle.putCharSequence(
"ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
"ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
"FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
"HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
"MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
"UNVISITED_LINK,VISITED_LINK");
}
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addChild(hostView, VIRTUAL_CONTENT_ID);
break;
default:
info.setParent(mView);
info.setSource(mView, virtualDescendantId);
info.setVisibleToUser(mView.isShown());
info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
info.setEnabled(true);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
break;
}
return info;
}
@Override
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
assertAttachedView(hostView);
if (virtualViewId == View.NO_ID) {
return performRootAction(action, arguments);
}
return performContentAction(action, arguments);
}
private boolean performRootAction(int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
final GeckoBundle data = new GeckoBundle(1);
data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
return true;
}
return mView.performAccessibilityAction(action, arguments);
}
@SuppressWarnings("fallthrough")
private boolean performContentAction(int action, Bundle arguments) {
final GeckoBundle data;
switch (action) {
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
return true;
case AccessibilityNodeInfo.ACTION_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
return true;
case AccessibilityNodeInfo.ACTION_LONG_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
return true;
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
return true;
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
return true;
case AccessibilityNodeInfo.ACTION_SELECT:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySelect", null);
return true;
case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
if (mLastItem) {
return false;
}
// fall-through
case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
if (arguments != null) {
data = new GeckoBundle(1);
data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
} else {
data = null;
}
mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
"GeckoView:AccessibilityNext" : "GeckoView:AccessibilityPrevious", data);
return true;
case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
// XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
// the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
// Other negative values are used by ChromeVox, but we don't support them.
// FAKE_GRANULARITY_READ_CURRENT = -1
// FAKE_GRANULARITY_READ_TITLE = -2
// FAKE_GRANULARITY_STOP_SPEECH = -3
// FAKE_GRANULARITY_CHANGE_SHIFTER = -4
int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
data = new GeckoBundle(1);
data.putInt("keyIndex", keyIndex);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
} else if (granularity > 0) {
boolean extendSelection = arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
data = new GeckoBundle(3);
data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
data.putInt("granularity", granularity);
data.putBoolean("select", extendSelection);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
}
return true;
case AccessibilityNodeInfo.ACTION_SET_SELECTION:
if (arguments == null) {
return false;
}
int selectionStart = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);
int selectionEnd = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT);
data = new GeckoBundle(2);
data.putInt("start", selectionStart);
data.putInt("end", selectionEnd);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySetSelection", data);
return true;
case AccessibilityNodeInfo.ACTION_CUT:
case AccessibilityNodeInfo.ACTION_COPY:
case AccessibilityNodeInfo.ACTION_PASTE:
data = new GeckoBundle(1);
data.putInt("action", action);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityClipboard", data);
return true;
}
return mView.performAccessibilityAction(action, arguments);
}
private void assertAttachedView(final View view) {
if (view != mView) {
throw new AssertionError("delegate used with wrong view.");
}
}
};
return mAccessibilityNodeProvider;
if (hostView != mView) {
return null;
}
if (mProvider == null) {
mProvider = new NodeProvider();
}
return mProvider;
}
});
}
private static class Settings {
private static final Settings INSTANCE = new Settings();
private static final String FORCE_ACCESSIBILITY_PREF = "accessibility.force_disabled";
private volatile boolean mEnabled;
/* package */ volatile boolean mForceEnabled;
private static volatile boolean sEnabled;
/* package */ static volatile boolean sForceEnabled;
public Settings() {
static {
final Context context = GeckoAppShell.getApplicationContext();
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = accessibilityManager.isEnabled() &&
accessibilityManager.isTouchExplorationEnabled();
accessibilityManager.addAccessibilityStateChangeListener(
new AccessibilityManager.AccessibilityStateChangeListener() {
@Override
@ -306,7 +295,7 @@ public class SessionAccessibility {
@Override
public void prefValue(String pref, int value) {
if (pref.equals(FORCE_ACCESSIBILITY_PREF)) {
mForceEnabled = value < 0;
sForceEnabled = value < 0;
dispatch();
}
}
@ -314,15 +303,11 @@ public class SessionAccessibility {
PrefsHelper.addObserver(new String[]{ FORCE_ACCESSIBILITY_PREF }, prefHandler);
}
public static Settings getInstance() {
return INSTANCE;
}
public static boolean isEnabled() {
return INSTANCE.mEnabled || INSTANCE.mForceEnabled;
return sEnabled || sForceEnabled;
}
private void updateAccessibilitySettings() {
public static void updateAccessibilitySettings() {
final AccessibilityManager accessibilityManager = (AccessibilityManager)
GeckoAppShell.getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
@ -330,9 +315,9 @@ public class SessionAccessibility {
dispatch();
}
private void dispatch() {
/* package */ static void dispatch() {
final GeckoBundle ret = new GeckoBundle(1);
ret.putBoolean("enabled", mEnabled || mForceEnabled);
ret.putBoolean("enabled", isTouchExplorationEnabled());
// "GeckoView:AccessibilitySettings" is dispatched to the Gecko thread.
EventDispatcher.getInstance().dispatch("GeckoView:AccessibilitySettings", ret);
// "GeckoView:AccessibilityEnabled" is dispatched to the UI thread.