All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowAccessibilityNodeInfo Maven / Gradle / Ivy

package org.robolectric.shadows;

import android.R.integer;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
import android.view.accessibility.AccessibilityNodeInfo.RangeInfo;
import android.view.accessibility.AccessibilityWindowInfo;

import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.internal.ShadowExtractor;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static org.robolectric.Shadows.shadowOf;
/**
 * Shadow of {@link android.view.accessibility.AccessibilityNodeInfo} that allows a test to set
 * properties that are locked in the original class. It also keeps track of calls to
 * {@code obtain()} and {@code recycle()} to look for bugs that mismatches.
 */
@Implements(AccessibilityNodeInfo.class)
public class ShadowAccessibilityNodeInfo {
  // Map of obtained instances of the class along with stack traces of how they were obtained
  private static final Map obtainedInstances =
      new HashMap<>();

  private static final SparseArray orderedInstances = new SparseArray<>();

  // Bitmasks for actions
  public static final int UNDEFINED_SELECTION_INDEX = -1;

  public static final Parcelable.Creator CREATOR =
      new Parcelable.Creator() {

    @Override
    public AccessibilityNodeInfo createFromParcel(Parcel source) {
      return obtain(orderedInstances.get(source.readInt()).mInfo);
    }

    @Override
    public AccessibilityNodeInfo[] newArray(int size) {
      return new AccessibilityNodeInfo[size];
    }};

  private static int sAllocationCount = 0;

  private static final int CLICKABLE_MASK = 0x00000001;

  private static final int LONGCLICKABLE_MASK = 0x00000002;

  private static final int FOCUSABLE_MASK = 0x00000004;

  private static final int FOCUSED_MASK = 0x00000008;

  private static final int VISIBLE_TO_USER_MASK = 0x00000010;

  private static final int SCROLLABLE_MASK = 0x00000020;

  private static final int PASTEABLE_MASK = 0x00000040;

  private static final int EDITABLE_MASK = 0x00000080;

  private static final int TEXT_SELECTION_SETABLE_MASK = 0x00000100;

  private static final int CHECKABLE_MASK = 0x00001000; //14

  private static final int CHECKED_MASK = 0x00002000; //14

  private static final int ENABLED_MASK = 0x00010000; //14

  private static final int PASSWORD_MASK = 0x00040000; //14

  private static final int SELECTED_MASK = 0x00080000; //14

  private static final int A11YFOCUSED_MASK = 0x00000800;  //16
  private static final int MULTILINE_MASK = 0x00020000; //19

  private static final int CONTENT_INVALID_MASK = 0x00004000; //19

  private static final int DISMISSABLE_MASK = 0x00008000; //19

  private static final int CAN_OPEN_POPUP_MASK = 0x00100000; //19

  private List children;

  private Rect boundsInScreen = new Rect();

  private Rect boundsInParent = new Rect();

  private List> performedActionAndArgsList;

  // In API prior to 21, actions are stored in a flag, after 21 they are stored in array of
  // AccessibilityAction so custom actions can be supported.
  private ArrayList actionsArray;
  // Storage of flags
  private int propertyFlags;

  private AccessibilityNodeInfo parent;

  private AccessibilityNodeInfo labelFor;

  private AccessibilityNodeInfo labeledBy;

  private View view;

  private CharSequence contentDescription;

  private CharSequence text;

  private CharSequence className;

  private int textSelectionStart = UNDEFINED_SELECTION_INDEX;

  private int textSelectionEnd = UNDEFINED_SELECTION_INDEX;

  private boolean refreshReturnValue = true;

  private int movementGranularities; //16

  private CharSequence packageName; //14
  private String viewIdResourceName; //18
  private CollectionInfo collectionInfo; //19

  private CollectionItemInfo collectionItemInfo; //19

  private int inputType; //19

  private int liveRegion; //19

  private RangeInfo rangeInfo; //19
  private int maxTextLength; //21

  private CharSequence error; //21
  
  private AccessibilityWindowInfo accessibilityWindowInfo;
  private AccessibilityNodeInfo traversalAfter; //22

  private AccessibilityNodeInfo traversalBefore; //22

  private OnPerformActionListener actionListener;
  
  private boolean visitedWhenCheckingChildren = false;

  @RealObject
  private AccessibilityNodeInfo realAccessibilityNodeInfo;

  public void __constructor__() {
    ReflectionHelpers.setStaticField(AccessibilityNodeInfo.class, "CREATOR", ShadowAccessibilityNodeInfo.CREATOR);
  }

  @Implementation
  public static AccessibilityNodeInfo obtain(AccessibilityNodeInfo info) {
    final ShadowAccessibilityNodeInfo shadowInfo =
        ((ShadowAccessibilityNodeInfo) ShadowExtractor.extract(info));
    final AccessibilityNodeInfo obtainedInstance = shadowInfo.getClone();

    sAllocationCount++;
    StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance);
    obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace());
    orderedInstances.put(sAllocationCount, wrapper);
    return obtainedInstance;
  }

  @Implementation
  public static AccessibilityNodeInfo obtain(View view) {
    // We explicitly avoid allocating the AccessibilityNodeInfo from the actual pool by using the
    // private constructor. Not doing so affects test suites which use both shadow and
    // non-shadow objects.
    final AccessibilityNodeInfo obtainedInstance =
        ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class);
    final ShadowAccessibilityNodeInfo shadowObtained =
        ((ShadowAccessibilityNodeInfo) ShadowExtractor.extract(obtainedInstance));

    /*
     * We keep a separate list of actions for each object newly obtained
     * from a view, and perform a shallow copy during getClone. That way the
     * list of actions performed contains all actions performed on the view
     * by the tree of nodes initialized from it. Note that initializing two
     * nodes with the same view will not merge the two lists, as so the list
     * of performed actions will not contain all actions performed on the
     * underlying view.
     */
    shadowObtained.performedActionAndArgsList = new LinkedList<>();

    shadowObtained.view = view;
    sAllocationCount++;
    StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance);
    obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace());
    orderedInstances.put(sAllocationCount, wrapper);
    return obtainedInstance;
  }

  @Implementation
  public static AccessibilityNodeInfo obtain() {
    return obtain(new View(RuntimeEnvironment.application.getApplicationContext()));
  }

  @Implementation
  public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) {
    AccessibilityNodeInfo node = obtain(root);
    return node;
  }

  /**
   * Check for leaked objects that were {@code obtain}ed but never
   * {@code recycle}d.
   *
   * @param printUnrecycledNodesToSystemErr - if true, stack traces of calls
   *        to {@code obtain} that lack matching calls to {@code recycle} are
   *        dumped to System.err.
   * @return {@code true} if there are unrecycled nodes
   */
  public static boolean areThereUnrecycledNodes(boolean printUnrecycledNodesToSystemErr) {
    if (printUnrecycledNodesToSystemErr) {
      for (final StrictEqualityNodeWrapper wrapper : obtainedInstances.keySet()) {
        final ShadowAccessibilityNodeInfo shadow =
            ((ShadowAccessibilityNodeInfo) ShadowExtractor.extract(wrapper.mInfo));

        System.err.println(String.format(
            "Leaked contentDescription = %s. Stack trace:", shadow.getContentDescription()));
        for (final StackTraceElement stackTraceElement : obtainedInstances.get(wrapper)) {
          System.err.println(stackTraceElement.toString());
        }
      }
    }

    return (obtainedInstances.size() != 0);
  }

  /**
   * Clear list of obtained instance objects. {@code areThereUnrecycledNodes}
   * will always return false if called immediately afterwards.
   */
  public static void resetObtainedInstances() {
    obtainedInstances.clear();
    orderedInstances.clear();
  }

  @Implementation
  public void recycle() {
    final StrictEqualityNodeWrapper wrapper =
        new StrictEqualityNodeWrapper(realAccessibilityNodeInfo);
    if (!obtainedInstances.containsKey(wrapper)) {
      throw new IllegalStateException();
    }

    if (labelFor != null) {
      labelFor.recycle();
    }

    if (labeledBy != null) {
      labeledBy.recycle();
    }
    if (traversalAfter != null) {
      traversalAfter.recycle();
    }

    if (traversalBefore != null) {
      traversalBefore.recycle();
    }
    obtainedInstances.remove(wrapper);
    int keyOfWrapper = -1;
    for (int i = 0; i < orderedInstances.size(); i++) {
      int key = orderedInstances.keyAt(i);
      if (orderedInstances.get(key).equals(wrapper)) {
        keyOfWrapper = key;
        break;
      }
    }
    orderedInstances.remove(keyOfWrapper);
  }

  @Implementation
  public int getChildCount() {
    if (children == null) {
      return 0;
    }

    return children.size();
  }

  @Implementation
  public AccessibilityNodeInfo getChild(int index) {
    if (children == null) {
      return null;
    }

    final AccessibilityNodeInfo child = children.get(index);
    if (child == null) {
      return null;
    }

    return obtain(child);
  }

  @Implementation
  public AccessibilityNodeInfo getParent() {
    if (parent == null) {
      return null;
    }

    return obtain(parent);
  }

  @Implementation
  public boolean refresh() {
      return refreshReturnValue;
  }

  public void setRefreshReturnValue(boolean refreshReturnValue) {
    this.refreshReturnValue = refreshReturnValue;
  }

  @Implementation
  public boolean isClickable() {
    return ((propertyFlags & CLICKABLE_MASK) != 0);
  }

  @Implementation
  public boolean isLongClickable() {
    return ((propertyFlags & LONGCLICKABLE_MASK) != 0);
  }

  @Implementation
  public boolean isFocusable() {
    return ((propertyFlags & FOCUSABLE_MASK) != 0);
  }

  @Implementation
  public boolean isFocused() {
    return ((propertyFlags & FOCUSED_MASK) != 0);
  }

  @Implementation
  public boolean isVisibleToUser() {
    return ((propertyFlags & VISIBLE_TO_USER_MASK) != 0);
  }

  @Implementation
  public boolean isScrollable() {
    return ((propertyFlags & SCROLLABLE_MASK) != 0);
  }

  public boolean isPasteable() {
    return ((propertyFlags & PASTEABLE_MASK) != 0);
  }

  @Implementation
  public boolean isEditable() {
    return ((propertyFlags & EDITABLE_MASK) != 0);
  }

  public boolean isTextSelectionSetable() {
    return ((propertyFlags & TEXT_SELECTION_SETABLE_MASK) != 0);
  }

  @Implementation
  public boolean isCheckable() {
    return ((propertyFlags & CHECKABLE_MASK) != 0);
  }

  @Implementation
  public void setCheckable(boolean checkable) {
    propertyFlags = (propertyFlags & ~CHECKABLE_MASK) |
        (checkable ? CHECKABLE_MASK : 0);
  }

  @Implementation
  public void setChecked(boolean checked) {
    propertyFlags = (propertyFlags & ~CHECKED_MASK) |
        (checked ? CHECKED_MASK : 0);
  }

  @Implementation
  public boolean isChecked() {
    return ((propertyFlags & CHECKED_MASK) != 0);
  }

  @Implementation
  public void setEnabled(boolean enabled) {
    propertyFlags = (propertyFlags & ~ENABLED_MASK) |
        (enabled ? ENABLED_MASK : 0);
  }

  @Implementation
  public boolean isEnabled() {
    return ((propertyFlags & ENABLED_MASK) != 0);
  }

  @Implementation
  public void setPassword(boolean password) {
    propertyFlags = (propertyFlags & ~PASSWORD_MASK) |
        (password ? PASSWORD_MASK : 0);
  }

  @Implementation
  public boolean isPassword() {
    return ((propertyFlags & PASSWORD_MASK) != 0);
  }

  @Implementation
  public void setSelected(boolean selected) {
    propertyFlags = (propertyFlags & ~SELECTED_MASK) |
        (selected ? SELECTED_MASK : 0);
  }

  @Implementation
  public boolean isSelected() {
    return ((propertyFlags & SELECTED_MASK) != 0);
  }

  @Implementation
  public void setAccessibilityFocused(boolean focused) {
    propertyFlags = (propertyFlags & ~A11YFOCUSED_MASK) |
        (focused ? A11YFOCUSED_MASK : 0);
  }

  @Implementation
  public boolean isAccessibilityFocused() {
    return ((propertyFlags & A11YFOCUSED_MASK) != 0);
  }
  @Implementation
  public void setMultiLine(boolean multiLine) {
    propertyFlags = (propertyFlags & ~MULTILINE_MASK) |
        (multiLine ? MULTILINE_MASK : 0);
  }

  @Implementation
  public boolean isMultiLine() {
    return ((propertyFlags & MULTILINE_MASK) != 0);
  }

  @Implementation
  public void setContentInvalid(boolean contentInvalid) {
    propertyFlags = (propertyFlags & ~CONTENT_INVALID_MASK) |
        (contentInvalid ? CONTENT_INVALID_MASK : 0);
  }

  @Implementation
  public boolean isContentInvalid() {
    return ((propertyFlags & CONTENT_INVALID_MASK) != 0);
  }

  @Implementation
  public void setDismissable(boolean dismissable) {
    propertyFlags = (propertyFlags & ~DISMISSABLE_MASK) |
        (dismissable ? DISMISSABLE_MASK : 0);
  }

  @Implementation
  public boolean isDismissable() {
    return ((propertyFlags & DISMISSABLE_MASK) != 0);
  }

  @Implementation
  public void setCanOpenPopup(boolean opensPopup) {
    propertyFlags = (propertyFlags & ~CAN_OPEN_POPUP_MASK) |
        (opensPopup ? CAN_OPEN_POPUP_MASK : 0);
  }

  @Implementation
  public boolean canOpenPopup() {
    return ((propertyFlags & CAN_OPEN_POPUP_MASK) != 0);
  }

  public void setTextSelectionSetable(boolean isTextSelectionSetable) {
    propertyFlags = (propertyFlags & ~TEXT_SELECTION_SETABLE_MASK) |
        (isTextSelectionSetable ? TEXT_SELECTION_SETABLE_MASK : 0);
  }

  @Implementation
  public void setClickable(boolean isClickable) {
    propertyFlags = (propertyFlags & ~CLICKABLE_MASK) | (isClickable ? CLICKABLE_MASK : 0);
  }

  @Implementation
  public void setLongClickable(boolean isLongClickable) {
    propertyFlags =
        (propertyFlags & ~LONGCLICKABLE_MASK) | (isLongClickable ? LONGCLICKABLE_MASK : 0);
  }

  @Implementation
  public void setFocusable(boolean isFocusable) {
    propertyFlags = (propertyFlags & ~FOCUSABLE_MASK) | (isFocusable ? FOCUSABLE_MASK : 0);
  }

  @Implementation
  public void setFocused(boolean isFocused) {
    propertyFlags = (propertyFlags & ~FOCUSED_MASK) | (isFocused ? FOCUSED_MASK : 0);
  }

  @Implementation
  public void setScrollable(boolean isScrollable) {
    propertyFlags = (propertyFlags & ~SCROLLABLE_MASK) | (isScrollable ? SCROLLABLE_MASK : 0);
  }

  public void setPasteable(boolean isPasteable) {
    propertyFlags = (propertyFlags & ~PASTEABLE_MASK) | (isPasteable ? PASTEABLE_MASK : 0);
  }

  @Implementation
  public void setEditable(boolean isEditable) {
    propertyFlags = (propertyFlags & ~EDITABLE_MASK) | (isEditable ? EDITABLE_MASK : 0);
  }

  @Implementation
  public void setVisibleToUser(boolean isVisibleToUser) {
    propertyFlags =
        (propertyFlags & ~VISIBLE_TO_USER_MASK) | (isVisibleToUser ? VISIBLE_TO_USER_MASK : 0);
  }

  @Implementation
  public void setContentDescription(CharSequence description) {
    contentDescription = description;
  }

  @Implementation
  public CharSequence getContentDescription() {
    return contentDescription;
  }

  @Implementation
  public void setClassName(CharSequence name) {
    className = name;
  }

  @Implementation
  public CharSequence getClassName() {
    return className;
  }

  @Implementation
  public void setText(CharSequence t) {
    text = t;
  }

  @Implementation
  public CharSequence getText() {
    return text;
  }

  @Implementation
  public void setTextSelection(int start, int end) {
      textSelectionStart = start;
      textSelectionEnd = end;
  }

  /**
   * Gets the text selection start.
   *
   * @return The text selection start if there is selection or UNDEFINED_SELECTION_INDEX.
   */
  @Implementation
  public int getTextSelectionStart() {
      return textSelectionStart;
  }

  /**
   * Gets the text selection end.
   *
   * @return The text selection end if there is selection or UNDEFINED_SELECTION_INDEX.
   */
  @Implementation
  public int getTextSelectionEnd() {
      return textSelectionEnd;
  }

  @Implementation
  public AccessibilityNodeInfo getLabelFor() {
    if (labelFor == null) {
      return null;
    }

    return obtain(labelFor);
  }

  public void setLabelFor(AccessibilityNodeInfo info) {
    if (labelFor != null) {
      labelFor.recycle();
    }

    labelFor = obtain(info);
  }

  @Implementation
  public AccessibilityNodeInfo getLabeledBy() {
    if (labeledBy == null) {
      return null;
    }

    return obtain(labeledBy);
  }

  public void setLabeledBy(AccessibilityNodeInfo info) {
    if (labeledBy != null) {
      labeledBy.recycle();
    }

    labeledBy = obtain(info);
  }

  @Implementation
  public int getMovementGranularities() {
    return movementGranularities;
  }

  @Implementation
  public void setMovementGranularities(int movementGranularities) {
    this.movementGranularities = movementGranularities;
  }

  @Implementation
  public CharSequence getPackageName() {
    return packageName;
  }

  @Implementation
  public void setPackageName(CharSequence packageName) {
    this.packageName = packageName;
  }

  @Implementation
  public String getViewIdResourceName() {
    return viewIdResourceName;
  }

  @Implementation
  public void setViewIdResourceName(String viewIdResourceName) {
    this.viewIdResourceName = viewIdResourceName;
  }
  @Implementation
  public CollectionInfo getCollectionInfo() {
    return collectionInfo;
  }

  @Implementation
  public void setCollectionInfo(CollectionInfo collectionInfo) {
    this.collectionInfo = collectionInfo;
  }

  @Implementation
  public CollectionItemInfo getCollectionItemInfo() {
    return collectionItemInfo;
  }

  @Implementation
  public void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) {
    this.collectionItemInfo = collectionItemInfo;
  }

  @Implementation
  public int getInputType() {
    return inputType;
  }

  @Implementation
  public void setInputType(int inputType) {
    this.inputType = inputType;
  }

  @Implementation
  public int getLiveRegion() {
    return liveRegion;
  }

  @Implementation
  public void setLiveRegion(int liveRegion) {
    this.liveRegion = liveRegion;
  }

  @Implementation
  public RangeInfo getRangeInfo() {
    return rangeInfo;
  }

  @Implementation
  public void setRangeInfo(RangeInfo rangeInfo) {
    this.rangeInfo = rangeInfo;
  }
  @Implementation
  public int getMaxTextLength() {
    return maxTextLength;
  }

  @Implementation
  public void setMaxTextLength(int maxTextLength) {
    this.maxTextLength = maxTextLength;
  }

  @Implementation
  public CharSequence getError() {
    return error;
  }

  @Implementation
  public void setError(CharSequence error) {
    this.error = error;
  }
  @Implementation
  public AccessibilityNodeInfo getTraversalAfter() {
    if (traversalAfter == null) {
      return null;
    }

    return obtain(traversalAfter);
  }

  @Implementation
  public void setTraversalAfter(AccessibilityNodeInfo info) {
    if (this.traversalAfter != null) {
      this.traversalAfter.recycle();
    }
    
    this.traversalAfter = obtain(info);
  }

  @Implementation
  public AccessibilityNodeInfo getTraversalBefore() {
    if (traversalBefore == null) {
      return null;
    }

    return obtain(traversalBefore);
  }

  @Implementation
  public void setTraversalBefore(AccessibilityNodeInfo info) {
    if (this.traversalBefore != null) {
      this.traversalBefore.recycle();
    }
    
    this.traversalBefore = obtain(info);
  }
  @Implementation
  public void setSource (View source) {
    this.view = source;
  }

  @Implementation
  public void setSource (View root, int virtualDescendantId) {
    this.view = root;
  }

  @Implementation
  public void getBoundsInScreen(Rect outBounds) {
    if (boundsInScreen == null) {
      boundsInScreen = new Rect();
    }
    outBounds.set(boundsInScreen);
  }

  @Implementation
  public void getBoundsInParent(Rect outBounds) {
    if (boundsInParent == null) {
      boundsInParent = new Rect();
    }
    outBounds.set(boundsInParent);
  }

  @Implementation
  public void setBoundsInScreen(Rect b) {
    if (boundsInScreen == null) {
      boundsInScreen = new Rect(b);
    } else {
      boundsInScreen.set(b);
    }
  }

  @Implementation
  public void setBoundsInParent(Rect b) {
    if (boundsInParent == null) {
      boundsInParent = new Rect(b);
    } else {
      boundsInParent.set(b);
    }
  }

  @Implementation
  public void addAction(int action) {
  if ((action & getActionTypeMaskFromFramework()) != 0) {
    throw new IllegalArgumentException("Action is not a combination of the standard " +
            "actions: " + action);
  }
  int remainingIds = action;
  while (remainingIds > 0) {
    final int id = 1 << Integer.numberOfTrailingZeros(remainingIds);
    remainingIds &= ~id;
    AccessibilityAction convertedAction = getActionFromIdFromFrameWork(id);
    addAction(convertedAction);
  }
  }

  @Implementation
  public void addAction(AccessibilityAction action) {
    if (action == null) {
      return;
    }

    if (actionsArray == null) {
      actionsArray = new ArrayList<>();
    }
    actionsArray.remove(action);
    actionsArray.add(action);
  }

  @Implementation
  public void removeAction(int action) {
    AccessibilityAction convertedAction = getActionFromIdFromFrameWork(action);
    removeAction(convertedAction);
  }

  @Implementation
  public boolean removeAction(AccessibilityAction action) {
    if (action == null || actionsArray == null) {
      return false;
    }
    return actionsArray.remove(action);
  }
  /**
   * Obtain flags for actions supported. Currently only supports ACTION_CLICK, ACTION_LONG_CLICK,
   * ACTION_SCROLL_FORWARD, ACTION_PASTE, ACTION_FOCUS, ACTION_SET_SELECTION, ACTION_SCROLL_BACKWARD
   * Returned value is derived from the getters.
   *
   * @return Action mask. 0 if no actions supported.
   */
  @Implementation
  public int getActions() {
    int returnValue = 0;
    if (actionsArray == null) {
      return returnValue;
    }

    // Custom actions are only returned by getActionsList
    final int actionSize = actionsArray.size();
    for (int i = 0; i < actionSize; i++) {
      int actionId = actionsArray.get(i).getId();
        if (actionId <= getLastLegacyActionFromFrameWork()) {
          returnValue |= actionId;
      }
    }
    return returnValue;
  }

  @Implementation
  public AccessibilityWindowInfo getWindow() {
    return accessibilityWindowInfo;
  }

  public void setAccessibilityWindowInfo(AccessibilityWindowInfo info) {
    accessibilityWindowInfo = info;
  }

  @Implementation
  public List getActionList() {
    if (actionsArray == null) {
      return Collections.emptyList();
    }

    return actionsArray;
  }

  @Implementation
  public boolean performAction(int action) {
    return performAction(action, null);
  }

  @Implementation
  public boolean performAction(int action, Bundle arguments) {
    if (performedActionAndArgsList == null) {
      performedActionAndArgsList = new LinkedList<>();
    }

    performedActionAndArgsList.add(new Pair(new Integer(action), arguments));
    return (actionListener != null) ? actionListener.onPerformAccessibilityAction(action, arguments)
      : true;
  }

  private boolean childrenEqualityCheck(
      ShadowAccessibilityNodeInfo otherShadow,
      LinkedList visitedNodes) {
    if (children.size() != otherShadow.children.size()) {
      return false;
    }
    boolean childrenEquality = true;
    for (int i = 0; i < children.size(); i++) {
      ShadowAccessibilityNodeInfo childShadow =
          (ShadowAccessibilityNodeInfo) ShadowExtractor.extract(children.get(i));
      if (!childShadow.visitedWhenCheckingChildren) {
        ShadowAccessibilityNodeInfo otherChildShadow =
            (ShadowAccessibilityNodeInfo) ShadowExtractor.extract(otherShadow.children.get(i));
        visitedNodes.add(childShadow);
        childShadow.visitedWhenCheckingChildren = true;
        if (!childShadow.equals(otherShadow.children.get(i))) {
           childrenEquality = false;
           break;
        }
        childrenEquality = childShadow.childrenEqualityCheck(otherChildShadow, visitedNodes);
      }
    }
    return childrenEquality;
  }

  /**
   * Equality check based on reference equality for mParent and mView and
   * value equality for other fields.
   */
  @Implementation
  @Override
  public boolean equals(Object object) {
    if (!(object instanceof AccessibilityNodeInfo)) {
      return false;
    }

    final AccessibilityNodeInfo info = (AccessibilityNodeInfo) object;
    final ShadowAccessibilityNodeInfo otherShadow =
        (ShadowAccessibilityNodeInfo) ShadowExtractor.extract(info);

    boolean areEqual = true;
    if (children == null) {
      areEqual &= (otherShadow.children == null);
    } else {
      LinkedList visitedNodes = new LinkedList<>();
      areEqual &=
          (otherShadow.children != null) && childrenEqualityCheck(otherShadow, visitedNodes);
      if (parent == null) {
        areEqual &= (otherShadow.parent == null);
      } else if (!shadowOf(parent).visitedWhenCheckingChildren){
        areEqual &= (shadowOf(parent).equals(otherShadow.parent));
      }

      while (!visitedNodes.isEmpty()) {
        ShadowAccessibilityNodeInfo visitedNode = visitedNodes.remove();
        visitedNode.visitedWhenCheckingChildren = false;
      }
    }
    areEqual &= (propertyFlags == otherShadow.propertyFlags);

    boolean actionsArrayEquality = false;
    if (actionsArray == null && otherShadow.actionsArray == null) {
      actionsArrayEquality = true;
    } else if (actionsArray == null || otherShadow.actionsArray == null) {
      actionsArrayEquality = false;
    } else {
      actionsArrayEquality = actionsArray.equals(otherShadow.actionsArray);
    }
    areEqual &= actionsArrayEquality;
    if (accessibilityWindowInfo == null) {
      areEqual &= (otherShadow.accessibilityWindowInfo == null);
    } else {
      areEqual &= (otherShadow.accessibilityWindowInfo != null) 
                      && accessibilityWindowInfo.equals(otherShadow.accessibilityWindowInfo);
    }
    /*
     * These checks have the potential to become infinite loops if there are
     * loops in the labelFor or labeledBy logic. Rather than deal with this
     * complexity, allow the failure since it will indicate a problem that
     * needs addressing.
     */
    if (labelFor == null) {
      areEqual &= (otherShadow.labelFor == null);
    } else {
      areEqual &= (labelFor.equals(otherShadow.labelFor));
    }

    if (labeledBy == null) {
      areEqual &= (otherShadow.labeledBy == null);
    } else {
      areEqual &= (labeledBy.equals(otherShadow.labeledBy));
    }

    areEqual &= boundsInScreen.equals(otherShadow.boundsInScreen);
    areEqual &= (TextUtils.equals(contentDescription, otherShadow.contentDescription));
    areEqual &= (TextUtils.equals(text, otherShadow.text));

    areEqual &= TextUtils.equals(className, otherShadow.className);
    areEqual &= (view == otherShadow.view);
    areEqual &= (textSelectionStart == otherShadow.textSelectionStart);
    areEqual &= (textSelectionEnd == otherShadow.textSelectionEnd);

    areEqual &= (refreshReturnValue == otherShadow.refreshReturnValue);
    areEqual &= (movementGranularities == otherShadow.movementGranularities);
    areEqual &= (TextUtils.isEmpty(packageName) == TextUtils.isEmpty(otherShadow.packageName));
    if (!TextUtils.isEmpty(packageName)) {
      areEqual &= (packageName.toString().equals(otherShadow.packageName.toString()));
    }
    areEqual &= TextUtils.equals(viewIdResourceName, otherShadow.viewIdResourceName);
    if (collectionInfo == null) {
      areEqual &= (otherShadow.collectionInfo == null);
    } else {
      areEqual &= (collectionInfo.equals(otherShadow.collectionInfo));
    }
    if (collectionItemInfo == null) {
      areEqual &= (otherShadow.collectionItemInfo == null);
    } else {
      areEqual &= (collectionItemInfo.equals(otherShadow.collectionItemInfo));
    }
    areEqual &= (inputType == otherShadow.inputType);
    areEqual &= (liveRegion == otherShadow.liveRegion);
    if (rangeInfo == null) {
      areEqual &= (otherShadow.rangeInfo == null);
    } else {
      areEqual &= (rangeInfo.equals(otherShadow.rangeInfo));
    }
    areEqual &= (maxTextLength == otherShadow.maxTextLength);
    areEqual &= TextUtils.equals(error, otherShadow.error);
    if (traversalAfter == null) {
      areEqual &= (otherShadow.traversalAfter == null);
    } else {
      areEqual &= (traversalAfter.equals(otherShadow.traversalAfter));
    }
    if (traversalBefore == null) {
      areEqual &= (otherShadow.traversalBefore == null);
    } else {
      areEqual &= (traversalBefore.equals(otherShadow.traversalBefore));
    }
    return areEqual;
  }

  @Implementation
  @Override
  public int hashCode() {
    // This is 0 for a reason. If you change it, you will break the obtained
    // instances map in a manner that is remarkably difficult to debug.
    // Having a dynamic hash code keeps this object from being located
    // in the map if it was mutated after being obtained.
    return 0;
  }

  /**
   * Add a child node to this one. Also initializes the parent field of the
   * child.
   *
   * @param child The node to be added as a child.
   */
  public void addChild(AccessibilityNodeInfo child) {
    if (children == null) {
      children = new LinkedList<>();
    }

    children.add(child);
    ((ShadowAccessibilityNodeInfo) ShadowExtractor.extract(child)).parent =
        realAccessibilityNodeInfo;
  }

  @Implementation
  public void addChild(View child) {
    AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(child);
    addChild(node);
  }

  @Implementation
  public void addChild(View root, int virtualDescendantId) {
    AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(root, virtualDescendantId);
    addChild(node);
  }

  /**
   * @return The list of arguments for the various calls to performAction. Unmodifiable.
   */
  public List getPerformedActions() {
    if (performedActionAndArgsList == null) {
      performedActionAndArgsList = new LinkedList<>();
    }

    // Here we take the actions out of the pairs and stick them into a separate LinkedList to return
    List actionsOnly = new LinkedList();
    Iterator> iter = performedActionAndArgsList.iterator();
    while (iter.hasNext()) {
      actionsOnly.add(iter.next().first);
    }

    return Collections.unmodifiableList(actionsOnly);
  }

  /**
   * @return The list of arguments for the various calls to performAction. Unmodifiable.
   */
  public List> getPerformedActionsWithArgs() {
    if (performedActionAndArgsList == null) {
      performedActionAndArgsList = new LinkedList<>();
    }
    return Collections.unmodifiableList(performedActionAndArgsList);
  }

  /**
   * @return A shallow copy.
   */
  private AccessibilityNodeInfo getClone() {
    // We explicitly avoid allocating the AccessibilityNodeInfo from the actual pool by using
    // the private constructor. Not doing so affects test suites which use both shadow and
    // non-shadow objects.
    final AccessibilityNodeInfo newInfo =
        ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class);
    final ShadowAccessibilityNodeInfo newShadow =
        (ShadowAccessibilityNodeInfo) ShadowExtractor.extract(newInfo);

    newShadow.boundsInScreen = new Rect(boundsInScreen);
    newShadow.propertyFlags = propertyFlags;
    newShadow.contentDescription = contentDescription;
    newShadow.text = text;
    newShadow.performedActionAndArgsList = performedActionAndArgsList;
    newShadow.parent = parent;
    newShadow.className = className;
    newShadow.labeledBy = labeledBy;
    newShadow.view = view;
    newShadow.textSelectionStart = textSelectionStart;
    newShadow.textSelectionEnd = textSelectionEnd;
    newShadow.actionListener = actionListener;
    if (actionsArray != null) {
      newShadow.actionsArray = new ArrayList<>();
      newShadow.actionsArray.addAll(actionsArray);
    } else {
      newShadow.actionsArray = null;
    }

    if (children != null) {
      newShadow.children = new LinkedList<>();
      newShadow.children.addAll(children);
    } else {
      newShadow.children = null;
    }

    newShadow.refreshReturnValue = refreshReturnValue;
    newShadow.movementGranularities = movementGranularities;
    newShadow.packageName = packageName;
    newShadow.viewIdResourceName = viewIdResourceName;
    newShadow.collectionInfo = collectionInfo;
    newShadow.collectionItemInfo = collectionItemInfo;
    newShadow.inputType = inputType;
    newShadow.liveRegion = liveRegion;
    newShadow.rangeInfo = rangeInfo;
    newShadow.maxTextLength = maxTextLength;
    newShadow.error = error;
    newShadow.traversalAfter = (traversalAfter == null) ? null : obtain(traversalAfter);
    newShadow.traversalBefore = (traversalBefore == null) ? null : obtain(traversalBefore);
    return newInfo;
  }

  /**
   * Private class to keep different nodes referring to the same view straight
   * in the mObtainedInstances map.
   */
  private static class StrictEqualityNodeWrapper {
    public final AccessibilityNodeInfo mInfo;

    public StrictEqualityNodeWrapper(AccessibilityNodeInfo info) {
      mInfo = info;
    }

    @Override
    public boolean equals(Object object) {
      if (object == null) {
        return false;
      }

      final StrictEqualityNodeWrapper wrapper = (StrictEqualityNodeWrapper) object;
      return mInfo == wrapper.mInfo;
    }

    @Override
    public int hashCode() {
      return mInfo.hashCode();
    }
  }

  /**
   * Shadow of AccessibilityAction.
   */
  @Implements(AccessibilityNodeInfo.AccessibilityAction.class)
  public static final class ShadowAccessibilityAction {
    private int id;
    private CharSequence label;

    public void __constructor__(int id, CharSequence label) {
      if (((id & (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "ACTION_TYPE_MASK")) == 0) && Integer.bitCount(id) != 1) {
        throw new IllegalArgumentException("Invalid standard action id");
      }
      this.id = id;
      this.label = label;
    }

    @Implementation
    public int getId() {
      return id;
    }

    @Implementation
    public CharSequence getLabel() {
      return label;
    }

    @Override
    @Implementation
    public boolean equals(Object other) {
      if (other == null) {
        return false;
      }

      if (other == this) {
        return true;
      }

      if (other.getClass() != AccessibilityAction.class) {
        return false;
      }

      return id == ((AccessibilityAction) other).getId();
    }

    @Override
    public String toString() {
      String actionSybolicName = ReflectionHelpers.callStaticMethod(
          AccessibilityNodeInfo.class, "getActionSymbolicName", ClassParameter.from(int.class, id));
      return "AccessibilityAction: " + actionSybolicName + " - " + label;
    }
  }
  @Implementation
  public int describeContents() {
    return 0;
  }

  @Implementation
  public void writeToParcel(Parcel dest, int flags) {
    StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(realAccessibilityNodeInfo);
    int keyOfWrapper = -1;
    for (int i = 0; i < orderedInstances.size(); i++) {
      if (orderedInstances.valueAt(i).equals(wrapper)) {
        keyOfWrapper = orderedInstances.keyAt(i);
        break;
      }
    }
    dest.writeInt(keyOfWrapper);
  }

  private static int getActionTypeMaskFromFramework() {
    // Get the mask to determine whether an int is a legit ID for an action, defined by Android
    return (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "ACTION_TYPE_MASK");
  }
  
  private static AccessibilityAction getActionFromIdFromFrameWork(int id) {
    // Convert an action ID to Android standard Accessibility Action defined by Android
    return ReflectionHelpers.callStaticMethod(
        AccessibilityNodeInfo.class, "getActionSingleton", ClassParameter.from(int.class, id));
  }
  
  private static int getLastLegacyActionFromFrameWork() {
    return (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "LAST_LEGACY_STANDARD_ACTION");
  }  
  /**
   * Configure the return result of an action if it is performed
   *
   * @param listener The listener.
   */
  public void setOnPerformActionListener(OnPerformActionListener listener) {
    actionListener = listener;
  }

  public interface OnPerformActionListener {
    boolean onPerformAccessibilityAction(int action, Bundle arguments);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy