Reorder the contents in ReactAccessibilityDelegate.java to be more logical (#49735)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49735

I always get confused when I open this file. One of the main reasons is the order of contents is quite random, so I decided to reorder things. I tried to find a style guide on this but the [google ones](https://google.github.io/styleguide/javaguide.html#s3.4.2-ordering-class-contents) really just say do something that is logical so...

In general what I did was

* All instance variables, class variables, class constants, etc. are at the top of the class
* The constructor, `setDelegate`, `resetDelegate` methods are at the top of the class since these all deal with creating or destroying instances of this class
* Nested enums towards the bottom of the class so they are not in the way of people trying to reason about the methods and other contents (they are quite long)
* Delete random extra spaces / unneeded comments
* Move some private methods to be closer to the public methods that call them

Changelog: [Internal]

Reviewed By: mlord93

Differential Revision: D70345882

fbshipit-source-id: a8425c7b90c5d4fbea5ee5daa3c4ff9f7f189ce5
This commit is contained in:
Joe Vilches
2025-02-27 15:38:15 -08:00
committed by Facebook GitHub Bot
parent 19c18eb995
commit 144f44f653
@@ -50,15 +50,25 @@ import java.util.List;
*/
public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
private static final String TAG = "ReactAccessibilityDelegate";
public static final String TOP_ACCESSIBILITY_ACTION_EVENT = "topAccessibilityAction";
public static final HashMap<String, Integer> sActionIdMap = new HashMap<>();
private static final String TAG = "ReactAccessibilityDelegate";
private static int sCounter = 0x3f000000;
private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
private static final int SEND_EVENT = 1;
private static final String delimiter = ", ";
private static final int delimiterLength = delimiter.length();
// State constants for states which have analogs in AccessibilityNodeInfo
private static final String STATE_DISABLED = "disabled";
private static final String STATE_SELECTED = "selected";
private static final String STATE_CHECKED = "checked";
public static final HashMap<String, Integer> sActionIdMap = new HashMap<>();
private final View mView;
private Handler mHandler;
private final HashMap<Integer, String> mAccessibilityActionsMap;
@Nullable View mAccessibilityLabelledBy;
static {
sActionIdMap.put("activate", AccessibilityActionCompat.ACTION_CLICK.getId());
@@ -69,312 +79,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
sActionIdMap.put("collapse", AccessibilityActionCompat.ACTION_COLLAPSE.getId());
}
private final View mView;
private Handler mHandler;
/**
* Schedule a command for sending an accessibility event. </br> Note: A command is used to ensure
* that accessibility events are sent at most one in a given time frame to save system resources
* while the progress changes quickly.
*/
private void scheduleAccessibilityEventSender(View host) {
if (mHandler.hasMessages(SEND_EVENT, host)) {
mHandler.removeMessages(SEND_EVENT, host);
}
Message msg = mHandler.obtainMessage(SEND_EVENT, host);
mHandler.sendMessageDelayed(msg, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
}
/**
* An ARIA Role representable by View's `role` prop. Ordinals should be kept in sync with
* `facebook::react::Role`.
*/
public enum Role {
ALERT,
ALERTDIALOG,
APPLICATION,
ARTICLE,
BANNER,
BUTTON,
CELL,
CHECKBOX,
COLUMNHEADER,
COMBOBOX,
COMPLEMENTARY,
CONTENTINFO,
DEFINITION,
DIALOG,
DIRECTORY,
DOCUMENT,
FEED,
FIGURE,
FORM,
GRID,
GROUP,
HEADING,
IMG,
LINK,
LIST,
LISTITEM,
LOG,
MAIN,
MARQUEE,
MATH,
MENU,
MENUBAR,
MENUITEM,
METER,
NAVIGATION,
NONE,
NOTE,
OPTION,
PRESENTATION,
PROGRESSBAR,
RADIO,
RADIOGROUP,
REGION,
ROW,
ROWGROUP,
ROWHEADER,
SCROLLBAR,
SEARCHBOX,
SEPARATOR,
SLIDER,
SPINBUTTON,
STATUS,
SUMMARY,
SWITCH,
TAB,
TABLE,
TABLIST,
TABPANEL,
TERM,
TIMER,
TOOLBAR,
TOOLTIP,
TREE,
TREEGRID,
TREEITEM;
public static @Nullable Role fromValue(@Nullable String value) {
for (Role role : Role.values()) {
if (role.name().equalsIgnoreCase(value)) {
return role;
}
}
return null;
}
}
/**
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
* date with their implementation. Details can be seen in their source code here:
*
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
*/
public enum AccessibilityRole {
NONE,
BUTTON,
DROPDOWNLIST,
TOGGLEBUTTON,
LINK,
SEARCH,
IMAGE,
IMAGEBUTTON,
KEYBOARDKEY,
TEXT,
ADJUSTABLE,
SUMMARY,
HEADER,
ALERT,
CHECKBOX,
COMBOBOX,
MENU,
MENUBAR,
MENUITEM,
PROGRESSBAR,
RADIO,
RADIOGROUP,
SCROLLBAR,
SPINBUTTON,
SWITCH,
TAB,
TABLIST,
TIMER,
LIST,
GRID,
PAGER,
SCROLLVIEW,
HORIZONTALSCROLLVIEW,
VIEWGROUP,
WEBVIEW,
DRAWERLAYOUT,
SLIDINGDRAWER,
ICONMENU,
TOOLBAR;
public static String getValue(AccessibilityRole role) {
switch (role) {
case BUTTON:
return "android.widget.Button";
case DROPDOWNLIST:
return "android.widget.Spinner";
case TOGGLEBUTTON:
return "android.widget.ToggleButton";
case SEARCH:
return "android.widget.EditText";
case IMAGE:
return "android.widget.ImageView";
case IMAGEBUTTON:
return "android.widget.ImageButton";
case KEYBOARDKEY:
return "android.inputmethodservice.Keyboard$Key";
case TEXT:
return "android.widget.TextView";
case ADJUSTABLE:
return "android.widget.SeekBar";
case CHECKBOX:
return "android.widget.CheckBox";
case RADIO:
return "android.widget.RadioButton";
case SPINBUTTON:
return "android.widget.SpinButton";
case SWITCH:
return "android.widget.Switch";
case LIST:
return "android.widget.AbsListView";
case GRID:
return "android.widget.GridView";
case SCROLLVIEW:
return "android.widget.ScrollView";
case HORIZONTALSCROLLVIEW:
return "android.widget.HorizontalScrollView";
case PAGER:
return "androidx.viewpager.widget.ViewPager";
case DRAWERLAYOUT:
return "androidx.drawerlayout.widget.DrawerLayout";
case SLIDINGDRAWER:
return "android.widget.SlidingDrawer";
case ICONMENU:
return "com.android.internal.view.menu.IconMenuView";
case VIEWGROUP:
return "android.view.ViewGroup";
case WEBVIEW:
return "android.webkit.WebView";
case NONE:
case LINK:
case SUMMARY:
case HEADER:
case ALERT:
case COMBOBOX:
case MENU:
case MENUBAR:
case MENUITEM:
case PROGRESSBAR:
case RADIOGROUP:
case SCROLLBAR:
case TAB:
case TABLIST:
case TIMER:
case TOOLBAR:
return "android.view.View";
default:
throw new IllegalArgumentException("Invalid accessibility role value: " + role);
}
}
public static AccessibilityRole fromValue(@Nullable String value) {
if (value == null) {
return NONE;
}
for (AccessibilityRole role : AccessibilityRole.values()) {
if (role.name().equalsIgnoreCase(value)) {
return role;
}
}
throw new IllegalArgumentException("Invalid accessibility role value: " + value);
}
public static @Nullable AccessibilityRole fromRole(Role role) {
switch (role) {
case ALERT:
return AccessibilityRole.ALERT;
case BUTTON:
return AccessibilityRole.BUTTON;
case CHECKBOX:
return AccessibilityRole.CHECKBOX;
case COMBOBOX:
return AccessibilityRole.COMBOBOX;
case GRID:
return AccessibilityRole.GRID;
case HEADING:
return AccessibilityRole.HEADER;
case IMG:
return AccessibilityRole.IMAGE;
case LINK:
return AccessibilityRole.LINK;
case LIST:
return AccessibilityRole.LIST;
case MENU:
return AccessibilityRole.MENU;
case MENUBAR:
return AccessibilityRole.MENUBAR;
case MENUITEM:
return AccessibilityRole.MENUITEM;
case NONE:
return AccessibilityRole.NONE;
case PROGRESSBAR:
return AccessibilityRole.PROGRESSBAR;
case RADIO:
return AccessibilityRole.RADIO;
case RADIOGROUP:
return AccessibilityRole.RADIOGROUP;
case SCROLLBAR:
return AccessibilityRole.SCROLLBAR;
case SEARCHBOX:
return AccessibilityRole.SEARCH;
case SLIDER:
return AccessibilityRole.ADJUSTABLE;
case SPINBUTTON:
return AccessibilityRole.SPINBUTTON;
case SUMMARY:
return AccessibilityRole.SUMMARY;
case SWITCH:
return AccessibilityRole.SWITCH;
case TAB:
return AccessibilityRole.TAB;
case TABLIST:
return AccessibilityRole.TABLIST;
case TIMER:
return AccessibilityRole.TIMER;
case TOOLBAR:
return AccessibilityRole.TOOLBAR;
default:
// No mapping from ARIA role to AccessibilityRole
return null;
}
}
public static @Nullable AccessibilityRole fromViewTag(View view) {
Role role = (Role) view.getTag(R.id.role);
if (role != null) {
return AccessibilityRole.fromRole(role);
} else {
return (AccessibilityRole) view.getTag(R.id.accessibility_role);
}
}
}
private final HashMap<Integer, String> mAccessibilityActionsMap;
// State constants for states which have analogs in AccessibilityNodeInfo
private static final String STATE_DISABLED = "disabled";
private static final String STATE_SELECTED = "selected";
private static final String STATE_CHECKED = "checked";
public ReactAccessibilityDelegate(
final View view, boolean originalFocus, int originalImportantForAccessibility) {
super(view);
@@ -397,13 +101,37 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
ViewCompat.setImportantForAccessibility(mView, originalImportantForAccessibility);
}
public static void setDelegate(
final View view, boolean originalFocus, int originalImportantForAccessibility) {
// if a view already has an accessibility delegate, replacing it could cause
// problems, so leave it alone.
if (!ViewCompat.hasAccessibilityDelegate(view)
&& (view.getTag(R.id.accessibility_role) != null
|| view.getTag(R.id.accessibility_state) != null
|| view.getTag(R.id.accessibility_actions) != null
|| view.getTag(R.id.react_test_id) != null
|| view.getTag(R.id.accessibility_collection_item) != null
|| view.getTag(R.id.accessibility_links) != null
|| view.getTag(R.id.role) != null)) {
ViewCompat.setAccessibilityDelegate(
view,
new ReactAccessibilityDelegate(view, originalFocus, originalImportantForAccessibility));
}
}
// Explicitly re-set the delegate, even if one has already been set.
public static void resetDelegate(
final View view, boolean originalFocus, int originalImportantForAccessibility) {
ViewCompat.setAccessibilityDelegate(
view,
new ReactAccessibilityDelegate(view, originalFocus, originalImportantForAccessibility));
}
// The View this delegate is attached to
protected View getHostView() {
return mView;
}
@Nullable View mAccessibilityLabelledBy;
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
@@ -608,6 +336,19 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
return super.performAccessibilityAction(host, action, args);
}
/**
* Schedule a command for sending an accessibility event. </br> Note: A command is used to ensure
* that accessibility events are sent at most one in a given time frame to save system resources
* while the progress changes quickly.
*/
private void scheduleAccessibilityEventSender(View host) {
if (mHandler.hasMessages(SEND_EVENT, host)) {
mHandler.removeMessages(SEND_EVENT, host);
}
Message msg = mHandler.obtainMessage(SEND_EVENT, host);
mHandler.sendMessageDelayed(msg, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
}
private static void setState(
AccessibilityNodeInfoCompat info, ReadableMap accessibilityState, Context context) {
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
@@ -626,10 +367,7 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
}
}
/** Strings for setting the Role Description in english */
// TODO: Eventually support for other languages on talkback
public static void setRole(
AccessibilityNodeInfoCompat nodeInfo, AccessibilityRole role, final Context context) {
if (role == null) {
@@ -681,33 +419,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
}
}
public static void setDelegate(
final View view, boolean originalFocus, int originalImportantForAccessibility) {
// if a view already has an accessibility delegate, replacing it could cause
// problems,
// so leave it alone.
if (!ViewCompat.hasAccessibilityDelegate(view)
&& (view.getTag(R.id.accessibility_role) != null
|| view.getTag(R.id.accessibility_state) != null
|| view.getTag(R.id.accessibility_actions) != null
|| view.getTag(R.id.react_test_id) != null
|| view.getTag(R.id.accessibility_collection_item) != null
|| view.getTag(R.id.accessibility_links) != null
|| view.getTag(R.id.role) != null)) {
ViewCompat.setAccessibilityDelegate(
view,
new ReactAccessibilityDelegate(view, originalFocus, originalImportantForAccessibility));
}
}
// Explicitly re-set the delegate, even if one has already been set.
public static void resetDelegate(
final View view, boolean originalFocus, int originalImportantForAccessibility) {
ViewCompat.setAccessibilityDelegate(
view,
new ReactAccessibilityDelegate(view, originalFocus, originalImportantForAccessibility));
}
@Override
protected int getVirtualViewAt(float x, float y) {
return INVALID_ID;
@@ -1027,4 +738,285 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
}
return builder.toString();
}
/**
* An ARIA Role representable by View's `role` prop. Ordinals should be kept in sync with
* `facebook::react::Role`.
*/
public enum Role {
ALERT,
ALERTDIALOG,
APPLICATION,
ARTICLE,
BANNER,
BUTTON,
CELL,
CHECKBOX,
COLUMNHEADER,
COMBOBOX,
COMPLEMENTARY,
CONTENTINFO,
DEFINITION,
DIALOG,
DIRECTORY,
DOCUMENT,
FEED,
FIGURE,
FORM,
GRID,
GROUP,
HEADING,
IMG,
LINK,
LIST,
LISTITEM,
LOG,
MAIN,
MARQUEE,
MATH,
MENU,
MENUBAR,
MENUITEM,
METER,
NAVIGATION,
NONE,
NOTE,
OPTION,
PRESENTATION,
PROGRESSBAR,
RADIO,
RADIOGROUP,
REGION,
ROW,
ROWGROUP,
ROWHEADER,
SCROLLBAR,
SEARCHBOX,
SEPARATOR,
SLIDER,
SPINBUTTON,
STATUS,
SUMMARY,
SWITCH,
TAB,
TABLE,
TABLIST,
TABPANEL,
TERM,
TIMER,
TOOLBAR,
TOOLTIP,
TREE,
TREEGRID,
TREEITEM;
public static @Nullable Role fromValue(@Nullable String value) {
for (Role role : Role.values()) {
if (role.name().equalsIgnoreCase(value)) {
return role;
}
}
return null;
}
}
/**
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
* date with their implementation. Details can be seen in their source code here:
*
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
*/
public enum AccessibilityRole {
NONE,
BUTTON,
DROPDOWNLIST,
TOGGLEBUTTON,
LINK,
SEARCH,
IMAGE,
IMAGEBUTTON,
KEYBOARDKEY,
TEXT,
ADJUSTABLE,
SUMMARY,
HEADER,
ALERT,
CHECKBOX,
COMBOBOX,
MENU,
MENUBAR,
MENUITEM,
PROGRESSBAR,
RADIO,
RADIOGROUP,
SCROLLBAR,
SPINBUTTON,
SWITCH,
TAB,
TABLIST,
TIMER,
LIST,
GRID,
PAGER,
SCROLLVIEW,
HORIZONTALSCROLLVIEW,
VIEWGROUP,
WEBVIEW,
DRAWERLAYOUT,
SLIDINGDRAWER,
ICONMENU,
TOOLBAR;
public static String getValue(AccessibilityRole role) {
switch (role) {
case BUTTON:
return "android.widget.Button";
case DROPDOWNLIST:
return "android.widget.Spinner";
case TOGGLEBUTTON:
return "android.widget.ToggleButton";
case SEARCH:
return "android.widget.EditText";
case IMAGE:
return "android.widget.ImageView";
case IMAGEBUTTON:
return "android.widget.ImageButton";
case KEYBOARDKEY:
return "android.inputmethodservice.Keyboard$Key";
case TEXT:
return "android.widget.TextView";
case ADJUSTABLE:
return "android.widget.SeekBar";
case CHECKBOX:
return "android.widget.CheckBox";
case RADIO:
return "android.widget.RadioButton";
case SPINBUTTON:
return "android.widget.SpinButton";
case SWITCH:
return "android.widget.Switch";
case LIST:
return "android.widget.AbsListView";
case GRID:
return "android.widget.GridView";
case SCROLLVIEW:
return "android.widget.ScrollView";
case HORIZONTALSCROLLVIEW:
return "android.widget.HorizontalScrollView";
case PAGER:
return "androidx.viewpager.widget.ViewPager";
case DRAWERLAYOUT:
return "androidx.drawerlayout.widget.DrawerLayout";
case SLIDINGDRAWER:
return "android.widget.SlidingDrawer";
case ICONMENU:
return "com.android.internal.view.menu.IconMenuView";
case VIEWGROUP:
return "android.view.ViewGroup";
case WEBVIEW:
return "android.webkit.WebView";
case NONE:
case LINK:
case SUMMARY:
case HEADER:
case ALERT:
case COMBOBOX:
case MENU:
case MENUBAR:
case MENUITEM:
case PROGRESSBAR:
case RADIOGROUP:
case SCROLLBAR:
case TAB:
case TABLIST:
case TIMER:
case TOOLBAR:
return "android.view.View";
default:
throw new IllegalArgumentException("Invalid accessibility role value: " + role);
}
}
public static AccessibilityRole fromValue(@Nullable String value) {
if (value == null) {
return NONE;
}
for (AccessibilityRole role : AccessibilityRole.values()) {
if (role.name().equalsIgnoreCase(value)) {
return role;
}
}
throw new IllegalArgumentException("Invalid accessibility role value: " + value);
}
public static @Nullable AccessibilityRole fromRole(Role role) {
switch (role) {
case ALERT:
return AccessibilityRole.ALERT;
case BUTTON:
return AccessibilityRole.BUTTON;
case CHECKBOX:
return AccessibilityRole.CHECKBOX;
case COMBOBOX:
return AccessibilityRole.COMBOBOX;
case GRID:
return AccessibilityRole.GRID;
case HEADING:
return AccessibilityRole.HEADER;
case IMG:
return AccessibilityRole.IMAGE;
case LINK:
return AccessibilityRole.LINK;
case LIST:
return AccessibilityRole.LIST;
case MENU:
return AccessibilityRole.MENU;
case MENUBAR:
return AccessibilityRole.MENUBAR;
case MENUITEM:
return AccessibilityRole.MENUITEM;
case NONE:
return AccessibilityRole.NONE;
case PROGRESSBAR:
return AccessibilityRole.PROGRESSBAR;
case RADIO:
return AccessibilityRole.RADIO;
case RADIOGROUP:
return AccessibilityRole.RADIOGROUP;
case SCROLLBAR:
return AccessibilityRole.SCROLLBAR;
case SEARCHBOX:
return AccessibilityRole.SEARCH;
case SLIDER:
return AccessibilityRole.ADJUSTABLE;
case SPINBUTTON:
return AccessibilityRole.SPINBUTTON;
case SUMMARY:
return AccessibilityRole.SUMMARY;
case SWITCH:
return AccessibilityRole.SWITCH;
case TAB:
return AccessibilityRole.TAB;
case TABLIST:
return AccessibilityRole.TABLIST;
case TIMER:
return AccessibilityRole.TIMER;
case TOOLBAR:
return AccessibilityRole.TOOLBAR;
default:
// No mapping from ARIA role to AccessibilityRole
return null;
}
}
public static @Nullable AccessibilityRole fromViewTag(View view) {
Role role = (Role) view.getTag(R.id.role);
if (role != null) {
return AccessibilityRole.fromRole(role);
} else {
return (AccessibilityRole) view.getTag(R.id.accessibility_role);
}
}
}
}