package org.robolectric.internal.bytecode;

import java.lang.invoke.MethodType;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.ListIterator;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.JSRInlinerAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.commons.Remapper;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;

public abstract class ClassInstrumentor {
  private static final String ROBO_INIT_METHOD_NAME = "$$robo$init";
  static final Type OBJECT_TYPE = Type.getType(Object.class);
  private static final ShadowImpl SHADOW_IMPL = new ShadowImpl();
  final Decorator decorator;

  protected ClassInstrumentor(Decorator decorator) {
    this.decorator = decorator;
  }

  public MutableClass analyzeClass(
      byte[] origClassBytes,
      final InstrumentationConfiguration config,
      ClassNodeProvider classNodeProvider) {
    ClassNode classNode =
        new ClassNode(Opcodes.ASM4) {
          @Override
          public FieldVisitor visitField(
              int access, String name, String desc, String signature, Object value) {
            desc = config.remapParamType(desc);
            return super.visitField(access & ~Opcodes.ACC_FINAL, name, desc, signature, value);
          }

          @Override
          public MethodVisitor visitMethod(
              int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor =
                super.visitMethod(access, name, config.remapParams(desc), signature, exceptions);
            return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
          }
        };

    final ClassReader classReader = new ClassReader(origClassBytes);
    classReader.accept(classNode, 0);
    return new MutableClass(classNode, config, classNodeProvider);
  }

  byte[] instrumentToBytes(MutableClass mutableClass) {
    instrument(mutableClass);

    ClassNode classNode = mutableClass.classNode;
    ClassWriter writer = new InstrumentingClassWriter(mutableClass.classNodeProvider, classNode);
    Remapper remapper =
        new Remapper() {
          @Override
          public String map(final String internalName) {
            return mutableClass.config.mappedTypeName(internalName);
          }
        };
    ClassRemapper visitor = new ClassRemapper(writer, remapper);
    classNode.accept(visitor);
    return writer.toByteArray();
  }

  public byte[] instrument(byte[] origBytes, InstrumentationConfiguration config,
      ClassNodeProvider classNodeProvider) {
    MutableClass mutableClass = analyzeClass(origBytes, config, classNodeProvider);
    return instrumentToBytes(mutableClass);
  }

  public void instrument(MutableClass mutableClass) {
    try {
      // no need to do anything to interfaces
      if (mutableClass.isInterface()) {
        return;
      }

      makeClassPublic(mutableClass.classNode);
      if ((mutableClass.classNode.access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL) {
        mutableClass.classNode.visitAnnotation(
            "Lcom/google/errorprone/annotations/DoNotMock;", true)
            .visit("value", "This class is final. Consider using the real thing, or "
                + "adding/enhancing a Robolectric shadow for it.");
      }
      mutableClass.classNode.access = mutableClass.classNode.access & ~Opcodes.ACC_FINAL;

      // Need Java version >=7 to allow invokedynamic
      mutableClass.classNode.version = Math.max(mutableClass.classNode.version, Opcodes.V1_7);

      instrumentMethods(mutableClass);

      // If there is no constructor, adds one
      addNoArgsConstructor(mutableClass);

      addDirectCallConstructor(mutableClass);

      addRoboInitMethod(mutableClass);

      decorator.decorate(mutableClass);

      doSpecialHandling(mutableClass);
    } catch (Exception e) {
      throw new RuntimeException("failed to instrument " + mutableClass.getName(), e);
    }
  }

  private void instrumentMethods(MutableClass mutableClass) {
    for (MethodNode method : mutableClass.getMethods()) {
      rewriteMethodBody(mutableClass, method);

      if (method.name.equals("<clinit>")) {
        method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME;
        mutableClass.addMethod(generateStaticInitializerNotifierMethod(mutableClass));
      } else if (method.name.equals("<init>")) {
        instrumentConstructor(mutableClass, method);
      } else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) {
        instrumentNormalMethod(mutableClass, method);
      }
    }
  }

  private void addNoArgsConstructor(MutableClass mutableClass) {
    if (!mutableClass.foundMethods.contains("<init>()V")) {
      MethodNode defaultConstructor = new MethodNode(Opcodes.ACC_PUBLIC, "<init>", "()V", "()V", null);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(defaultConstructor);
      generator.loadThis();
      generator.visitMethodInsn(Opcodes.INVOKESPECIAL, mutableClass.classNode.superName, "<init>", "()V", false);
      generator.loadThis();
      generator.invokeVirtual(mutableClass.classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
      generator.returnValue();
      mutableClass.addMethod(defaultConstructor);
    }
  }

  protected abstract void addDirectCallConstructor(MutableClass mutableClass);

  /**
   * Generates code like this:
   * ```java
   * protected void $$robo$init() {
   *   if (__robo_data__ == null) {
   *     __robo_data__ = RobolectricInternals.initializing(this);
   *   }
   * }
   * ```
   */
  private void addRoboInitMethod(MutableClass mutableClass) {
    MethodNode initMethodNode = new MethodNode(Opcodes.ACC_PROTECTED, ROBO_INIT_METHOD_NAME, "()V", null, null);
    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
    Label alreadyInitialized = new Label();
    generator.loadThis();                                         // this
    generator.getField(mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of __robo_data__
    generator.ifNonNull(alreadyInitialized);
    generator.loadThis();                                         // this
    generator.loadThis();                                         // this, this
    writeCallToInitializing(mutableClass, generator);
    // this, __robo_data__
    generator.putField(mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
    generator.mark(alreadyInitialized);
    generator.returnValue();
    mutableClass.addMethod(initMethodNode);
  }

  protected abstract void writeCallToInitializing(MutableClass mutableClass, RobolectricGeneratorAdapter generator);

  private void doSpecialHandling(MutableClass mutableClass) {
    if (mutableClass.getName().equals("android.os.Build$VERSION")) {
      for (FieldNode fieldNode : mutableClass.getFields()) {
        fieldNode.access &= ~(Modifier.FINAL);
      }
    }
  }

  private boolean isSyntheticAccessorMethod(MethodNode method) {
    return (method.access & Opcodes.ACC_SYNTHETIC) != 0;
  }

  /**
   * Constructors are instrumented as follows:
   * # Code other than a call to the superclass constructor is moved to a new method named
   *   `__constructor__` with the same signature.
   * # The constructor is modified to call {@link ClassHandler#initializing(Object)} (or
   *   {@link ClassHandler#getShadowCreator(Class)} for `invokedynamic` JVMs).
   * # The constructor is modified to then call
   *   {@link ClassHandler#methodInvoked(String, boolean, Class)} (or
   *   {@link ClassHandler#findShadowMethodHandle(Class, String, MethodType, boolean)} for
   *   `invokedynamic` JVMs) with the method name `__constructor__` and the same parameter types.
   *
   * Note that most code in the constructor will not be executed unless the {@link ClassHandler}
   * arranges for it to happen.
   *
   * Given a constructor like this:
   * ```java
   * public ThisClass(String name, int size) {
   *   super(name, someStaticMethod());
   *   this.size = size;
   * }
   * ```
   *
   * ... generates code like this:
   * ```java
   * private $$robo$$__constructor__(String name, int size) {
   *   this.size = size;
   * }
   *
   * private __constructor__(String name, int size) {
   *   Plan plan = RobolectricInternals.methodInvoked(
   *       "pkg/ThisClass/__constructor__(Ljava/lang/String;I)V", true, ThisClass.class);
   *   if (plan != null) {
   *     try {
   *       plan.run(this, new Object[] {name, size});
   *     } catch (Throwable t) {
   *       throw RobolectricInternals.cleanStackTrace(t);
   *     }
   *   } else {
   *     $$robo$$__constructor__(name, size);
   *   }
   * }
   *
   * public ThisClass(String name, int size) {
   *   super(name, someStaticMethod());
   *   $$robo$init();
   * }
   * ```
   *
   * @param method the constructor to instrument
   */
  private void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
    makeMethodPrivate(method);

    if (mutableClass.containsStubs) {
      // method.instructions just throws a `stub!` exception, replace it with something anodyne...
      method.instructions.clear();

      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
      generator.loadThis();
      generator.visitMethodInsn(Opcodes.INVOKESPECIAL, mutableClass.classNode.superName, "<init>", "()V", false);
      generator.returnValue();
      generator.endMethod();
    }

    InsnList callSuper = extractCallToSuperConstructor(mutableClass, method);
    method.name = directMethodName(mutableClass, ShadowConstants.CONSTRUCTOR_METHOD_NAME);
    mutableClass.addMethod(redirectorMethod(mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME));

    String[] exceptions = exceptionArray(method);
    MethodNode initMethodNode = new MethodNode(method.access, "<init>", method.desc, method.signature, exceptions);
    makeMethodPublic(initMethodNode);
    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);

    initMethodNode.instructions = callSuper;

    generator.loadThis();
    generator.invokeVirtual(mutableClass.classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
    generateClassHandlerCall(mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME, generator);

    generator.endMethod();
    mutableClass.addMethod(initMethodNode);
  }

  private InsnList extractCallToSuperConstructor(MutableClass mutableClass, MethodNode ctor) {
    InsnList removedInstructions = new InsnList();
    int startIndex = 0;

    AbstractInsnNode[] insns = ctor.instructions.toArray();
    for (int i = 0; i < insns.length; i++) {
      AbstractInsnNode node = insns[i];

      switch (node.getOpcode()) {
        case Opcodes.ALOAD:
          VarInsnNode vnode = (VarInsnNode) node;
          if (vnode.var == 0) {
            startIndex = i;
          }
          break;

        case Opcodes.INVOKESPECIAL:
          MethodInsnNode mnode = (MethodInsnNode) node;
          if (mnode.owner.equals(mutableClass.internalClassName) || mnode.owner.equals(mutableClass.classNode.superName)) {
            assert mnode.name.equals("<init>");

            // remove all instructions in the range startIndex..i, from aload_0 to invokespecial <init>
            while (startIndex <= i) {
              ctor.instructions.remove(insns[startIndex]);
              removedInstructions.add(insns[startIndex]);
              startIndex++;
            }
            return removedInstructions;
          }
          break;

        case Opcodes.ATHROW:
          ctor.visitCode();
          ctor.visitInsn(Opcodes.RETURN);
          ctor.visitEnd();
          return removedInstructions;

        default:
          // nothing to do
      }
    }

    throw new RuntimeException("huh? " + ctor.name + ctor.desc);
  }

  /**
   * # Rename the method from `methodName` to `$$robo$$methodName`.
   * # Make it private so we can invoke it directly without subclass overrides taking precedence.
   * # Remove `final` modifiers, if present.
   * # Create a delegator method named `methodName` which delegates to the {@link ClassHandler}.
   */
  protected void instrumentNormalMethod(MutableClass mutableClass, MethodNode method) {
    // if not abstract, set a final modifier
    if ((method.access & Opcodes.ACC_ABSTRACT) == 0) {
      method.access = method.access | Opcodes.ACC_FINAL;
    }
    if ((method.access & Opcodes.ACC_NATIVE) != 0) {
      instrumentNativeMethod(mutableClass, method);
    }

    // todo figure out
    String originalName = method.name;
    method.name = directMethodName(mutableClass, originalName);

    MethodNode delegatorMethodNode = new MethodNode(method.access, originalName, method.desc, method.signature, exceptionArray(method));
    delegatorMethodNode.visibleAnnotations = method.visibleAnnotations;
    delegatorMethodNode.access &= ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL);

    makeMethodPrivate(method);

    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(delegatorMethodNode);
    generateClassHandlerCall(mutableClass, method, originalName, generator);
    generator.endMethod();
    mutableClass.addMethod(delegatorMethodNode);
  }

  /**
   * Creates native stub which returns the default return value.
   *
   * @param mutableClass Class to be instrumented
   * @param method Method to be instrumented, must be native
   */
  protected void instrumentNativeMethod(MutableClass mutableClass, MethodNode method) {
    method.access = method.access & ~Opcodes.ACC_NATIVE;

    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
    Type returnType = generator.getReturnType();
    generator.pushDefaultReturnValueToStack(returnType);
    generator.returnValue();
  }

  private String directMethodName(MutableClass mutableClass, String originalName) {
    return SHADOW_IMPL.directMethodName(mutableClass.getName(), originalName);
  }

  //todo rename
  private MethodNode redirectorMethod(MutableClass mutableClass, MethodNode method, String newName) {
    MethodNode redirector = new MethodNode(Opcodes.ASM4, newName, method.desc, method.signature, exceptionArray(method));
    redirector.access = method.access & ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL);
    makeMethodPrivate(redirector);
    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(redirector);
    generator.invokeMethod(mutableClass.internalClassName, method);
    generator.returnValue();
    return redirector;
  }

  protected String[] exceptionArray(MethodNode method) {
    List<String> exceptions = method.exceptions;
    return exceptions.toArray(new String[exceptions.size()]);
  }

  /**
   * Filters methods that might need special treatment because of various reasons
   */
  private void rewriteMethodBody(MutableClass mutableClass, MethodNode callingMethod) {
    ListIterator<AbstractInsnNode> instructions = callingMethod.instructions.iterator();
    while (instructions.hasNext()) {
      AbstractInsnNode node = instructions.next();

      switch (node.getOpcode()) {
        case Opcodes.NEW:
          TypeInsnNode newInsnNode = (TypeInsnNode) node;
          newInsnNode.desc = mutableClass.config.mappedTypeName(newInsnNode.desc);
          break;

        case Opcodes.GETFIELD:
          /* falls through */
        case Opcodes.PUTFIELD:
          /* falls through */
        case Opcodes.GETSTATIC:
          /* falls through */
        case Opcodes.PUTSTATIC:
          FieldInsnNode fieldInsnNode = (FieldInsnNode) node;
          fieldInsnNode.desc = mutableClass.config.mappedTypeName(fieldInsnNode.desc); // todo test
          break;

        case Opcodes.INVOKESTATIC:
          /* falls through */
        case Opcodes.INVOKEINTERFACE:
          /* falls through */
        case Opcodes.INVOKESPECIAL:
          /* falls through */
        case Opcodes.INVOKEVIRTUAL:
          MethodInsnNode targetMethod = (MethodInsnNode) node;
          targetMethod.desc = mutableClass.config.remapParams(targetMethod.desc);
          if (isGregorianCalendarBooleanConstructor(targetMethod)) {
            replaceGregorianCalendarBooleanConstructor(instructions, targetMethod);
          } else if (mutableClass.config.shouldIntercept(targetMethod)) {
            interceptInvokeVirtualMethod(mutableClass, instructions, targetMethod);
          }
          break;

        case Opcodes.INVOKEDYNAMIC:
          /* no unusual behavior */
          break;

        default:
          break;
      }
    }
  }

  /**
   * Verifies if the @targetMethod is a `<init>(boolean)` constructor for
   * {@link java.util.GregorianCalendar}.
   */
  private boolean isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod) {
    return targetMethod.owner.equals("java/util/GregorianCalendar") &&
        targetMethod.name.equals("<init>") &&
        targetMethod.desc.equals("(Z)V");
  }

  /**
   * Replaces the void `<init>(boolean)` constructor for a call to the
   * `void <init>(int, int, int)` one.
   */
  private void replaceGregorianCalendarBooleanConstructor(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
    // Remove the call to GregorianCalendar(boolean)
    instructions.remove();

    // Discard the already-pushed parameter for GregorianCalendar(boolean)
    instructions.add(new InsnNode(Opcodes.POP));

    // Add parameters values for calling GregorianCalendar(int, int, int)
    instructions.add(new InsnNode(Opcodes.ICONST_0));
    instructions.add(new InsnNode(Opcodes.ICONST_0));
    instructions.add(new InsnNode(Opcodes.ICONST_0));

    // Call GregorianCalendar(int, int, int)
    instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, targetMethod.owner, targetMethod.name, "(III)V", targetMethod.itf));
  }

  /**
   * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL
   * Opcode, depending if the invokedynamic bytecode instruction is available (Java 7+).
   */
  protected abstract void interceptInvokeVirtualMethod(
      MutableClass mutableClass, ListIterator<AbstractInsnNode> instructions,
      MethodInsnNode targetMethod);

  /**
   * Replaces protected and private class modifiers with public.
   */
  private void makeClassPublic(ClassNode clazz) {
    clazz.access = (clazz.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
  }

  /**
   * Replaces protected and private method modifiers with public.
   */
  protected void makeMethodPublic(MethodNode method) {
    method.access = (method.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
  }

  /**
   * Replaces protected and public class modifiers with private.
   */
  protected void makeMethodPrivate(MethodNode method) {
    method.access = (method.access | Opcodes.ACC_PRIVATE) & ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED);
  }

  private MethodNode generateStaticInitializerNotifierMethod(MutableClass mutableClass) {
    MethodNode methodNode = new MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()V", "()V", null);
    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
    generator.push(mutableClass.classType);
    generator.invokeStatic(Type.getType(RobolectricInternals.class), new Method("classInitializing", "(Ljava/lang/Class;)V"));
    generator.returnValue();
    generator.endMethod();
    return methodNode;
  }

  // todo javadocs
  protected abstract void generateClassHandlerCall(MutableClass mutableClass,
      MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator);

  int getTag(MethodNode m) {
    return Modifier.isStatic(m.access) ? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKESPECIAL;
  }

  public interface Decorator {
    void decorate(MutableClass mutableClass);

    void decorateMethodPreClassHandler(MutableClass mutableClass, MethodNode originalMethod,
        String originalMethodName, RobolectricGeneratorAdapter generator);
  }

  /**
   * Provides try/catch code generation with a {@link org.objectweb.asm.commons.GeneratorAdapter}.
   */
  static class TryCatch {
    private final Label start;
    private final Label end;
    private final Label handler;
    private final GeneratorAdapter generatorAdapter;

    TryCatch(GeneratorAdapter generatorAdapter, Type type) {
      this.generatorAdapter = generatorAdapter;
      this.start = generatorAdapter.mark();
      this.end = new Label();
      this.handler = new Label();
      generatorAdapter.visitTryCatchBlock(start, end, handler, type.getInternalName());
    }

    void end() {
      generatorAdapter.mark(end);
    }

    void handler() {
      generatorAdapter.mark(handler);
    }
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.N;
import static org.robolectric.RuntimeEnvironment.getApiLevel;

import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/**
 * Properties of {@link android.view.accessibility.AccessibilityNodeInfo} that are normally locked
 * may be changed using test APIs.
 *
 * Calls to {@code obtain()} and {@code recycle()} are tracked to help spot bugs.
 */
@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<StrictEqualityNodeWrapper, StackTraceElement[]> obtainedInstances =
      new HashMap<>();

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

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

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

    @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

  /**
   * Uniquely identifies the origin of the AccessibilityNodeInfo for equality
   * testing. Two instances that come from the same node info should have the
   * same ID.
   */
  private long mOriginNodeId;

  private List<AccessibilityNodeInfo> children;

  private Rect boundsInScreen = new Rect();

  private Rect boundsInParent = new Rect();

  private List<Pair<Integer, Bundle>> 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<AccessibilityAction> actionsArray;
  private int actionsMask;
  // 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 int drawingOrder; // 24

  @RealObject
  private AccessibilityNodeInfo realAccessibilityNodeInfo;

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

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

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

  @Implementation
  protected 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 = Shadow.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 ArrayList<>();

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

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

  @Implementation
  protected 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 = Shadow.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.
   */
  @Resetter
  public static void resetObtainedInstances() {
    obtainedInstances.clear();
    orderedInstances.clear();
  }

  @Implementation
  protected 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 (getApiLevel() >= LOLLIPOP_MR1) {
      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
  protected int getChildCount() {
    if (children == null) {
      return 0;
    }

    return children.size();
  }

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

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

    return obtain(child);
  }

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

    return obtain(parent);
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected boolean refresh() {
      return refreshReturnValue;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  @Implementation
  protected boolean isAccessibilityFocused() {
    return ((propertyFlags & A11YFOCUSED_MASK) != 0);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void setMultiLine(boolean multiLine) {
    propertyFlags = (propertyFlags & ~MULTILINE_MASK) |
        (multiLine ? MULTILINE_MASK : 0);
  }

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

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

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

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

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

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

  @Implementation(minSdk = LOLLIPOP)
  protected 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
  protected void setClickable(boolean isClickable) {
    propertyFlags = (propertyFlags & ~CLICKABLE_MASK) | (isClickable ? CLICKABLE_MASK : 0);
  }

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

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

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

  @Implementation
  protected 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(minSdk = JELLY_BEAN_MR2)
  protected void setEditable(boolean isEditable) {
    propertyFlags = (propertyFlags & ~EDITABLE_MASK) | (isEditable ? EDITABLE_MASK : 0);
  }

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

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

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

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

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

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

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

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected 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(minSdk = JELLY_BEAN_MR2)
  protected int getTextSelectionStart() {
      return textSelectionStart;
  }

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

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected AccessibilityNodeInfo getLabelFor() {
    if (labelFor == null) {
      return null;
    }

    return obtain(labelFor);
  }

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

    labelFor = obtain(info);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected AccessibilityNodeInfo getLabeledBy() {
    if (labeledBy == null) {
      return null;
    }

    return obtain(labeledBy);
  }

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

    labeledBy = obtain(info);
  }

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

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

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

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

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected String getViewIdResourceName() {
    return viewIdResourceName;
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected void setViewIdResourceName(String viewIdResourceName) {
    this.viewIdResourceName = viewIdResourceName;
  }

  @Implementation(minSdk = KITKAT)
  protected CollectionInfo getCollectionInfo() {
    return collectionInfo;
  }

  @Implementation(minSdk = KITKAT)
  protected void setCollectionInfo(CollectionInfo collectionInfo) {
    this.collectionInfo = collectionInfo;
  }

  @Implementation(minSdk = KITKAT)
  protected CollectionItemInfo getCollectionItemInfo() {
    return collectionItemInfo;
  }

  @Implementation(minSdk = KITKAT)
  protected void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) {
    this.collectionItemInfo = collectionItemInfo;
  }

  @Implementation(minSdk = KITKAT)
  protected int getInputType() {
    return inputType;
  }

  @Implementation(minSdk = KITKAT)
  protected void setInputType(int inputType) {
    this.inputType = inputType;
  }

  @Implementation(minSdk = KITKAT)
  protected int getLiveRegion() {
    return liveRegion;
  }

  @Implementation(minSdk = KITKAT)
  protected void setLiveRegion(int liveRegion) {
    this.liveRegion = liveRegion;
  }

  @Implementation(minSdk = KITKAT)
  protected RangeInfo getRangeInfo() {
    return rangeInfo;
  }

  @Implementation(minSdk = KITKAT)
  protected void setRangeInfo(RangeInfo rangeInfo) {
    this.rangeInfo = rangeInfo;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected int getMaxTextLength() {
    return maxTextLength;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void setMaxTextLength(int maxTextLength) {
    this.maxTextLength = maxTextLength;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected CharSequence getError() {
    return error;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void setError(CharSequence error) {
    this.error = error;
  }

  @Implementation(minSdk = LOLLIPOP_MR1)
  protected AccessibilityNodeInfo getTraversalAfter() {
    if (traversalAfter == null) {
      return null;
    }

    return obtain(traversalAfter);
  }

  @Implementation(minSdk = LOLLIPOP_MR1)
  protected void setTraversalAfter(View view, int virtualDescendantId) {
    if (this.traversalAfter != null) {
      this.traversalAfter.recycle();
    }

    this.traversalAfter = obtain(view);
  }

  /**
   * Sets the view whose node is visited after this one in accessibility traversal.
   *
   * This may be useful for configuring traversal order in tests before the corresponding
   * views have been inflated.
   *
   * @param info The previous node.
   * @see #getTraversalAfter()
   */
  public void setTraversalAfter(AccessibilityNodeInfo info) {
    if (this.traversalAfter != null) {
      this.traversalAfter.recycle();
    }

    this.traversalAfter = obtain(info);
  }

  @Implementation(minSdk = LOLLIPOP_MR1)
  protected AccessibilityNodeInfo getTraversalBefore() {
    if (traversalBefore == null) {
      return null;
    }

    return obtain(traversalBefore);
  }

  @Implementation(minSdk = LOLLIPOP_MR1)
  protected void setTraversalBefore(View info, int virtualDescendantId) {
    if (this.traversalBefore != null) {
      this.traversalBefore.recycle();
    }

    this.traversalBefore = obtain(info);
  }

  /**
   * Sets the view before whose node this one should be visited during traversal.
   *
   * This may be useful for configuring traversal order in tests before the corresponding
   * views have been inflated.
   *
   * @param info The view providing the preceding node.
   * @see #getTraversalBefore()
   */
  public void setTraversalBefore(AccessibilityNodeInfo info) {
    if (this.traversalBefore != null) {
      this.traversalBefore.recycle();
    }

    this.traversalBefore = obtain(info);
  }

  @Implementation
  protected void setSource(View source) {
    this.view = source;
  }

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

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

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

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

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

  @Implementation
  protected void addAction(int action) {
    if (getApiLevel() >= LOLLIPOP) {
      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);
      }
    } else {
      actionsMask |= action;
    }
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void addAction(AccessibilityAction action) {
    if (action == null) {
      return;
    }

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

  @Implementation(minSdk = LOLLIPOP)
  protected void removeAction(int action) {
    AccessibilityAction convertedAction = getActionFromIdFromFrameWork(action);
    removeAction(convertedAction);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected boolean removeAction(AccessibilityAction action) {
    if (action == null || actionsArray == null) {
      return false;
    }
    return actionsArray.remove(action);
  }

  /**
   * Obtain flags for actions supported. Currently only supports {@link
   * AccessibilityNodeInfo#ACTION_CLICK}, {@link AccessibilityNodeInfo#ACTION_LONG_CLICK}, {@link
   * AccessibilityNodeInfo#ACTION_SCROLL_FORWARD}, {@link AccessibilityNodeInfo#ACTION_PASTE},
   * {@link AccessibilityNodeInfo#ACTION_FOCUS}, {@link AccessibilityNodeInfo#ACTION_SET_SELECTION},
   * {@link AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD} Returned value is derived from the
   * getters.
   *
   * @return Action mask. 0 if no actions supported.
   */
  @Implementation
  protected int getActions() {
    if (getApiLevel() >= LOLLIPOP) {
      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;
    } else {
      return actionsMask;
    }
  }

  /** Returns the drawing order of the view corresponding to this node. */
  @Implementation(minSdk = N)
  protected int getDrawingOrder() {
    return drawingOrder;
  }

  /** Sets the drawing order of the view corresponding to this node. */
  @Implementation(minSdk = N)
  protected void setDrawingOrder(int drawingOrder) {
    this.drawingOrder = drawingOrder;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected AccessibilityWindowInfo getWindow() {
    return accessibilityWindowInfo;
  }

  /** Returns the id of the window from which the info comes. */
  @Implementation
  protected int getWindowId() {
    return (accessibilityWindowInfo == null) ? -1 : accessibilityWindowInfo.getId();
  }

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

  @Implementation(minSdk = LOLLIPOP)
  protected List<AccessibilityAction> getActionList() {
    if (actionsArray == null) {
      return Collections.emptyList();
    }

    return actionsArray;
  }

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

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

    performedActionAndArgsList.add(new Pair<>(action, arguments));
    return actionListener == null || actionListener.onPerformAccessibilityAction(action, arguments);
  }

  /**
   * Equality check based on reference equality of the Views from which these instances were
   * created, or the equality of their assigned IDs.
   */
  @Implementation
  @Override
  public boolean equals(Object object) {
    if (!(object instanceof AccessibilityNodeInfo)) {
      return false;
    }

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

    if (this.view != null) {
      return this.view == otherShadow.view;
    }
    if (this.mOriginNodeId != 0) {
      return this.mOriginNodeId == otherShadow.mOriginNodeId;
    }
    throw new IllegalStateException("Node has neither an ID nor View");
  }

  @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 ArrayList<>();
    }

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

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

  @Implementation
  protected 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<Integer> getPerformedActions() {
    if (performedActionAndArgsList == null) {
      performedActionAndArgsList = new ArrayList<>();
    }

    // Here we take the actions out of the pairs and stick them into a separate LinkedList to return
    List<Integer> actionsOnly = new ArrayList<>();
    Iterator<Pair<Integer, Bundle>> 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<Pair<Integer, Bundle>> getPerformedActionsWithArgs() {
    if (performedActionAndArgsList == null) {
      performedActionAndArgsList = new ArrayList<>();
    }
    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 = Shadow.extract(newInfo);

    newShadow.mOriginNodeId = mOriginNodeId;
    newShadow.boundsInScreen = new Rect(boundsInScreen);
    newShadow.propertyFlags = propertyFlags;
    newShadow.contentDescription = contentDescription;
    newShadow.text = text;
    newShadow.performedActionAndArgsList = performedActionAndArgsList;
    newShadow.parent = parent;
    newShadow.className = className;
    newShadow.labelFor = (labelFor == null) ? null : obtain(labelFor);
    newShadow.labeledBy = (labeledBy == null) ? null : obtain(labeledBy);
    newShadow.view = view;
    newShadow.textSelectionStart = textSelectionStart;
    newShadow.textSelectionEnd = textSelectionEnd;
    newShadow.actionListener = actionListener;
    if (getApiLevel() >= LOLLIPOP) {
      if (actionsArray != null) {
        newShadow.actionsArray = new ArrayList<>();
        newShadow.actionsArray.addAll(actionsArray);
      } else {
        newShadow.actionsArray = null;
      }
    } else {
      newShadow.actionsMask = actionsMask;
    }

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

    newShadow.refreshReturnValue = refreshReturnValue;
    newShadow.movementGranularities = movementGranularities;
    newShadow.packageName = packageName;
    if (getApiLevel() >= JELLY_BEAN_MR2) {
      newShadow.viewIdResourceName = viewIdResourceName;
    }
    if (getApiLevel() >= KITKAT) {
      newShadow.collectionInfo = collectionInfo;
      newShadow.collectionItemInfo = collectionItemInfo;
      newShadow.inputType = inputType;
      newShadow.liveRegion = liveRegion;
      newShadow.rangeInfo = rangeInfo;
      newShadow.realAccessibilityNodeInfo.getExtras().putAll(realAccessibilityNodeInfo.getExtras());
    }
    if (getApiLevel() >= LOLLIPOP) {
      newShadow.maxTextLength = maxTextLength;
      newShadow.error = error;
    }
    if (getApiLevel() >= LOLLIPOP_MR1) {
      newShadow.traversalAfter = (traversalAfter == null) ? null : obtain(traversalAfter);
      newShadow.traversalBefore = (traversalBefore == null) ? null : obtain(traversalBefore);
    }
    if ((getApiLevel() >= LOLLIPOP) && (accessibilityWindowInfo != null)) {
      newShadow.accessibilityWindowInfo =
          ShadowAccessibilityWindowInfo.obtain(accessibilityWindowInfo);
    }
    if (getApiLevel() >= N) {
      newShadow.drawingOrder = drawingOrder;
    }

    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
    @SuppressWarnings("ReferenceEquality")
    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(value = AccessibilityNodeInfo.AccessibilityAction.class, minSdk = LOLLIPOP)
  public static final class ShadowAccessibilityAction {
    private int id;
    private CharSequence label;

    @Implementation
    protected 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
    protected int getId() {
      return id;
    }

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

    @Override
    @Implementation
    @SuppressWarnings("EqualsHashCode")
    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
  protected int describeContents() {
    return 0;
  }

  @Implementation
  protected 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);
  }
}


package org.robolectric.internal.bytecode;

import static org.objectweb.asm.Type.ARRAY;
import static org.objectweb.asm.Type.OBJECT;

import java.lang.reflect.Modifier;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.tree.MethodNode;
import org.robolectric.internal.bytecode.ClassInstrumentor.TryCatch;

/**
 * GeneratorAdapter implementation specific to generate code for Robolectric purposes
 */
public class RobolectricGeneratorAdapter extends GeneratorAdapter {
  final boolean isStatic;
  private final String desc;

  public RobolectricGeneratorAdapter(MethodNode methodNode) {
    super(Opcodes.ASM4, methodNode, methodNode.access, methodNode.name, methodNode.desc);
    this.isStatic = Modifier.isStatic(methodNode.access);
    this.desc = methodNode.desc;
  }

  public void loadThisOrNull() {
    if (isStatic) {
      loadNull();
    } else {
      loadThis();
    }
  }

  public boolean isStatic() {
    return isStatic;
  }

  public void loadNull() {
    visitInsn(Opcodes.ACONST_NULL);
  }

  public Type getReturnType() {
    return Type.getReturnType(desc);
  }

  /**
   * Forces a return of a default value, depending on the method's return type
   *
   * @param type The method's return type
   */
  public void pushDefaultReturnValueToStack(Type type) {
    if (type.equals(Type.BOOLEAN_TYPE)) {
      push(false);
    } else if (type.equals(Type.INT_TYPE) || type.equals(Type.SHORT_TYPE) || type.equals(Type.BYTE_TYPE) || type.equals(Type.CHAR_TYPE)) {
      push(0);
    } else if (type.equals(Type.LONG_TYPE)) {
      push(0L);
    } else if (type.equals(Type.FLOAT_TYPE)) {
      push(0f);
    } else if (type.equals(Type.DOUBLE_TYPE)) {
      push(0d);
    } else if (type.getSort() == ARRAY || type.getSort() == OBJECT) {
      loadNull();
    }
  }

  public void invokeMethod(String internalClassName, MethodNode method) {
    invokeMethod(internalClassName, method.name, method.desc);
  }

  void invokeMethod(String internalClassName, String methodName, String methodDesc) {
    if (isStatic()) {
      loadArgs();                                             // this, [args]
      visitMethodInsn(Opcodes.INVOKESTATIC, internalClassName, methodName, methodDesc, false);
    } else {
      loadThisOrNull();                                       // this
      loadArgs();                                             // this, [args]
      visitMethodInsn(Opcodes.INVOKESPECIAL, internalClassName, methodName, methodDesc, false);
    }
  }

  public TryCatch tryStart(Type exceptionType) {
    return new TryCatch(this, exceptionType);
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.shadow.api.Shadow.directlyOn;

import android.net.NetworkCapabilities;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/** Robolectic provides overrides for fetching and updating transport. */
@Implements(value = NetworkCapabilities.class, minSdk = LOLLIPOP)
public class ShadowNetworkCapabilities {

  @RealObject protected NetworkCapabilities realNetworkCapabilities;

  public static NetworkCapabilities newInstance() {
    return Shadow.newInstanceOf(NetworkCapabilities.class);
  }

  /** Updates the transport types for this network capablities to include {@code transportType}. */
  @HiddenApi
  @Implementation
  public NetworkCapabilities addTransportType(int transportType) {
    return directlyOn(
        realNetworkCapabilities,
        NetworkCapabilities.class,
        "addTransportType",
        ClassParameter.from(int.class, transportType));
  }

  /** Updates the transport types for this network capablities to remove {@code transportType}. */
  @HiddenApi
  @Implementation
  public NetworkCapabilities removeTransportType(int transportType) {
    return directlyOn(
        realNetworkCapabilities,
        NetworkCapabilities.class,
        "removeTransportType",
        ClassParameter.from(int.class, transportType));
  }
}


package org.robolectric.res.android;

import static org.robolectric.res.android.Asset.toIntExact;
import static org.robolectric.res.android.Errors.NAME_NOT_FOUND;
import static org.robolectric.res.android.Errors.NO_ERROR;
import static org.robolectric.res.android.Util.ALOGW;
import static org.robolectric.res.android.Util.isTruthy;

import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class ZipFileRO {

  static final int kCompressStored = 0;
  static final int kCompressDeflated = 8;

  final ZipArchiveHandle mHandle;
  final String mFileName;

  ZipFileRO(ZipArchiveHandle handle, String fileName) {
    this.mHandle = handle;
    this.mFileName = fileName;
  }

  static class ZipEntryRO {
        ZipEntry entry;
    String name;
    Object cookie;

    ZipEntryRO() {
    }

    //    ~ZipEntryRO() {
    @Override
    protected void finalize() {
//      EndIteration(cookie);
    }

//    private:
//    ZipEntryRO(final ZipEntryRO& other);
//    ZipEntryRO& operator=(final ZipEntryRO& other);
  };

//  ~ZipFileRO() {
  @Override
  protected void finalize() {
    CloseArchive(mHandle);
//    free(mFileName);
  }

  static int OpenArchive(String zipFileName, Ref<ZipArchiveHandle> mHandle) {
    try {
      mHandle.set(new ZipArchiveHandle(new ZipFile(zipFileName)));
      return NO_ERROR;
    } catch (IOException e) {
      return NAME_NOT_FOUND;
    }
  }

  private static void CloseArchive(ZipArchiveHandle mHandle) {
    throw new UnsupportedOperationException();
  }

  private static String ErrorCodeString(int error) {
    return "error " + error;
  }

  static int FindEntry(ZipArchiveHandle mHandle, String name, Ref<ZipEntry> zipEntryRef) {
    ZipEntry entry = mHandle.zipFile.getEntry(name);
    zipEntryRef.set(entry);
    if (entry == null) {
      return NAME_NOT_FOUND;
    }
    return NO_ERROR;
  }

  /*
 * Open the specified file read-only.  We memory-map the entire thing and
 * close the file before returning.
 */
/* static */
  static ZipFileRO open(final String zipFileName)
  {
    final Ref<ZipArchiveHandle> handle = new Ref<>(null);
    final int error = OpenArchive(zipFileName, handle);
    if (isTruthy(error)) {
      ALOGW("Error opening archive %s: %s", zipFileName, ErrorCodeString(error));
      CloseArchive(handle.get());
      return null;
    }

    return new ZipFileRO(handle.get(), zipFileName);
  }

  // /* static */ ZipFileRO* ZipFileRO::openFd(int fd, String debugFileName,
  //     boolean assume_ownership)
  // {
  //   ZipArchiveHandle handle;
  //   int error = OpenArchiveFd(fd, debugFileName, &handle, assume_ownership);
  //   if (error) {
  //     ALOGW("Error opening archive fd %d %s: %s", fd, debugFileName, ErrorCodeString(error));
  //     CloseArchive(handle);
  //     return NULL;
  //   }
  //
  //   return new ZipFileRO(handle, strdup(debugFileName));
  // }

  org.robolectric.res.android.ZipFileRO.ZipEntryRO findEntryByName(final String entryName)
  {
    ZipEntryRO data = new ZipEntryRO();

    data.name = String(entryName);

    final Ref<ZipEntry> zipEntryRef = new Ref<>(data.entry);
    final int error = FindEntry(mHandle, data.name, zipEntryRef);
    if (isTruthy(error)) {
      return null;
    }

    data.entry = zipEntryRef.get();
    return data;
  }

  /*
   * Get the useful fields from the zip entry.
   *
   * Returns "false" if the offsets to the fields or the contents of the fields
   * appear to be bogus.
   */
  boolean getEntryInfo(org.robolectric.res.android.ZipFileRO.ZipEntryRO entry, Ref<Short> pMethod,
      final Ref<Long> pUncompLen, Ref<Long> pCompLen, Ref<Long> pOffset,
      final Ref<Long> pModWhen, Ref<Long> pCrc32)
  {
    final ZipEntryRO zipEntry = /*reinterpret_cast<ZipEntryRO*>*/(entry);
    final ZipEntry ze = zipEntry.entry;

    if (pMethod != null) {
      pMethod.set((short) ze.getMethod());
    }
    if (pUncompLen != null) {
        pUncompLen.set(ze.getSize()); // uncompressed_length
    }
    if (pCompLen != null) {
        pCompLen.set(ze.getCompressedSize());
    }
    if (pOffset != null) {
      throw new UnsupportedOperationException("Figure out offset");
      //        pOffset = ze.offset;
    }
    if (pModWhen != null) {
        // todo pModWhen.set(ze.getLastModifiedTime().toMillis());
    }
    if (pCrc32 != null) {
      pCrc32.set(ze.getCrc());
    }

    return true;
  }

  boolean startIteration(Ref<Enumeration<? extends ZipEntry>> cookie) {
    return startIteration(cookie, null, null);
  }

  boolean startIteration(/* void** */ Ref<Enumeration<? extends ZipEntry>> cookie, final String prefix, final String suffix)
  {
    cookie.set(this.mHandle.zipFile.entries());
//    ZipEntryRO* ze = new ZipEntryRO;
//    String pe(prefix ? prefix : "");
//    String se(suffix ? suffix : "");
//    int error = StartIteration(mHandle, &(ze.cookie),
//    prefix ? &pe : null,
//      suffix ? &se : null);
//    if (error) {
//      ALOGW("Could not start iteration over %s: %s", mFileName, ErrorCodeString(error));
//      delete ze;
//      return false;
//    }
//
//    *cookie = ze;
    return true;
  }

  org.robolectric.res.android.ZipFileRO.ZipEntryRO nextEntry(/*void* */ Enumeration<? extends ZipEntry> cookie)
  {
    if (!cookie.hasMoreElements()) {
      return null;
    }
    ZipEntryRO zipEntryRO = new ZipEntryRO();
    zipEntryRO.entry = cookie.nextElement();
    return zipEntryRO;
//    ZipEntryRO ze = /*reinterpret_cast<ZipEntryRO*>*/(ZipEntryRO) cookie;
//    int error = Next(ze.cookie, &(ze.entry), &(ze.name));
//    if (error) {
//      if (error != -1) {
//        ALOGW("Error iteration over %s: %s", mFileName, ErrorCodeString(error));
//      }
//      return null;
//    }
//
//    return &(ze.entry);
  }

  void endIteration(/*void**/ Object cookie)
  {
//    delete reinterpret_cast<ZipEntryRO*>(cookie);
  }

  void releaseEntry(org.robolectric.res.android.ZipFileRO.ZipEntryRO entry)
  {
//    delete reinterpret_cast<ZipEntryRO*>(entry);
  }

  /*
   * Copy the entry's filename to the buffer.
   */
  int getEntryFileName(org.robolectric.res.android.ZipFileRO.ZipEntryRO entry, Ref<String> buffer)
  {
    buffer.set(entry.entry.getName());

//    final ZipEntryRO* zipEntry = reinterpret_cast<ZipEntryRO*>(entry);
//    final uint16_t requiredSize = zipEntry.name.name_length + 1;
//
//    if (bufLen < requiredSize) {
//      ALOGW("Buffer too short, requires %d bytes for entry name", requiredSize);
//      return requiredSize;
//    }
//
//    memcpy(buffer, zipEntry.name.name, requiredSize - 1);
//    buffer[requiredSize - 1] = '\0';
//
    return 0;
  }

/*
 * Create a new FileMap object that spans the data in "entry".
 */
  /*FileMap*/ ZipFileRO(org.robolectric.res.android.ZipFileRO.ZipEntryRO entry)
  {
    throw new UnsupportedOperationException("Implememnt me");

//    final ZipEntryRO *zipEntry = reinterpret_cast<ZipEntryRO*>(entry);
//    final ZipEntry& ze = zipEntry.entry;
//    int fd = GetFileDescriptor(mHandle);
//    size_t actualLen = 0;
//
//    if (ze.method == kCompressStored) {
//      actualLen = ze.uncompressed_length;
//    } else {
//      actualLen = ze.compressed_length;
//    }
//
//    FileMap* newMap = new FileMap();
//    if (!newMap.create(mFileName, fd, ze.offset, actualLen, true)) {
//      delete newMap;
//      return null;
//    }
//
//    return newMap;
  }

  /*
 * Create a new FileMap object that spans the data in "entry".
 */
  FileMap createEntryFileMap(ZipEntryRO entry)
  {
    // final _ZipEntryRO *zipEntry = reinterpret_cast<_ZipEntryRO*>(entry);
    // const ZipEntry& ze = zipEntry->entry;
    ZipEntry ze = entry.entry;
    // int fd = GetFileDescriptor(mHandle);
    int fd = -1;
    int actualLen = 0;

    if (ze.getMethod() == kCompressStored) {
      actualLen = toIntExact(ze.getSize());
    } else {
      actualLen = toIntExact(ze.getCompressedSize());
    }

    FileMap newMap = new FileMap();
    if (!newMap.createFromZip(mFileName, mHandle.zipFile, entry.entry, actualLen, true)) {
      // delete newMap;
      return null;
    }

    return newMap;
  }

  /*
   * Uncompress an entry, in its entirety, into the provided output buffer.
   *
   * This doesn't verify the data's CRC, which might be useful for
   * uncompressed data.  The caller should be able to manage it.
   */
  boolean uncompressEntry(org.robolectric.res.android.ZipFileRO.ZipEntryRO entry, Object buffer, int size)
  {
    throw new UnsupportedOperationException("Implememnt me");
//    ZipEntryRO *zipEntry = reinterpret_cast<ZipEntryRO*>(entry);
//    final int error = ExtractToMemory(mHandle, &(zipEntry.entry),
//    (uint8_t*) buffer, size);
//    if (error) {
//      ALOGW("ExtractToMemory failed with %s", ErrorCodeString(error));
//      return false;
//    }
//
//    return true;
  }

  /*
   * Uncompress an entry, in its entirety, to an open file descriptor.
   *
   * This doesn't verify the data's CRC, but probably should.
   */
  boolean uncompressEntry(org.robolectric.res.android.ZipFileRO.ZipEntryRO entry, int fd)
  {
    throw new UnsupportedOperationException("Implememnt me");
//    ZipEntryRO *zipEntry = reinterpret_cast<ZipEntryRO*>(entry);
//    final int error = ExtractEntryToFile(mHandle, &(zipEntry.entry), fd);
//    if (error) {
//      ALOGW("ExtractToMemory failed with %s", ErrorCodeString(error));
//      return false;
//    }
//
//    return true;
  }

  static String String(String string) {
    return string;
  }
}


package org.robolectric.shadows;

import java.io.File;

/**
 * Provides a utility class for OsConstants See
 * https://unix.superglobalmegacorp.com/Net2/newsrc/sys/stat.h.html.
 */
final class OsConstantsValues {

  private OsConstantsValues() {}

  // Type of file.
  public static final String S_IFMT = "S_IFMT";

  // Directory.
  public static final String S_IFDIR = "S_IFDIR";

  // Regular file.
  public static final String S_IFREG = "S_IFREG";

  // Symbolic link.
  public static final String S_IFLNK = "S_IFLNK";

  // Type of file value.
  public static final int S_IFMT_VALUE = 0x0170000;

  // Directory value.
  public static final int S_IFDIR_VALUE = 0x0040000;

  // Regular file value.
  public static final int S_IFREG_VALUE = 0x0100000;

  // Link value.
  public static final int S_IFLNK_VALUE = 0x0120000;

  /** Returns the st_mode for the path. */
  public static int getMode(String path) {
    if (path == null) {
      return 0;
    }

    File file = new File(path);
    if (file.isDirectory()) {
      return S_IFDIR_VALUE;
    }
    if (file.isFile()) {
      return S_IFREG_VALUE;
    }
    if (!canonicalize(path).equals(path)) {
      return S_IFLNK_VALUE;
    }
    return 0;
  }

  private static String canonicalize(String path) {
    try {
      return new File(path).getCanonicalPath();
    } catch (Throwable t) {
      throw new RuntimeException(t);
    }
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.shadow.api.Shadow.directlyOn;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter.LeScanCallback;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.ParcelUuid;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(BluetoothAdapter.class)
public class ShadowBluetoothAdapter {
  @RealObject private BluetoothAdapter realAdapter;

  private static final int ADDRESS_LENGTH = 17;

  private Set<BluetoothDevice> bondedDevices = new HashSet<BluetoothDevice>();
  private Set<LeScanCallback> leScanCallbacks = new HashSet<LeScanCallback>();
  private boolean isDiscovering;
  private String address;
  private boolean enabled;
  private int state;
  private String name = "DefaultBluetoothDeviceName";
  private int scanMode = BluetoothAdapter.SCAN_MODE_NONE;
  private boolean isMultipleAdvertisementSupported = true;
  private boolean isOverridingProxyBehavior;
  private final Map<Integer, Integer> profileConnectionStateData = new HashMap<>();
  private final Map<Integer, BluetoothProfile> profileProxies = new HashMap<>();

  @Implementation
  protected static BluetoothAdapter getDefaultAdapter() {
    return (BluetoothAdapter) ShadowApplication.getInstance().getBluetoothAdapter();
  }

  @Implementation
  protected Set<BluetoothDevice> getBondedDevices() {
    return Collections.unmodifiableSet(bondedDevices);
  }

  public void setBondedDevices(Set<BluetoothDevice> bluetoothDevices) {
    bondedDevices = bluetoothDevices;
  }

  @Implementation
  protected BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(
      String serviceName, UUID uuid) {
    return ShadowBluetoothServerSocket.newInstance(
        BluetoothSocket.TYPE_RFCOMM, /*auth=*/ false, /*encrypt=*/ false, new ParcelUuid(uuid));
  }

  @Implementation
  protected BluetoothServerSocket listenUsingRfcommWithServiceRecord(String serviceName, UUID uuid)
      throws IOException {
    return ShadowBluetoothServerSocket.newInstance(
        BluetoothSocket.TYPE_RFCOMM, /*auth=*/ false, /*encrypt=*/ true, new ParcelUuid(uuid));
  }

  @Implementation
  protected boolean startDiscovery() {
    isDiscovering = true;
    return true;
  }

  @Implementation
  protected boolean cancelDiscovery() {
    isDiscovering = false;
    return true;
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected boolean startLeScan(LeScanCallback callback) {
    return startLeScan(null, callback);
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected boolean startLeScan(UUID[] serviceUuids, LeScanCallback callback) {
    // Ignoring the serviceUuids param for now.
    leScanCallbacks.add(callback);
    return true;
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected void stopLeScan(LeScanCallback callback) {
    leScanCallbacks.remove(callback);
  }

  public Set<LeScanCallback> getLeScanCallbacks() {
    return Collections.unmodifiableSet(leScanCallbacks);
  }

  public LeScanCallback getSingleLeScanCallback() {
    if (leScanCallbacks.size() != 1) {
      throw new IllegalStateException("There are " + leScanCallbacks.size() + " callbacks");
    }
    return leScanCallbacks.iterator().next();
  }

  @Implementation
  protected boolean isDiscovering() {
    return isDiscovering;
  }

  @Implementation
  protected boolean isEnabled() {
    return enabled;
  }

  @Implementation
  protected boolean enable() {
    enabled = true;
    return true;
  }

  @Implementation
  protected boolean disable() {
    enabled = false;
    return true;
  }

  @Implementation
  protected String getAddress() {
    return this.address;
  }

  @Implementation
  protected int getState() {
    return state;
  }

  @Implementation
  protected String getName() {
    return name;
  }

  @Implementation
  protected boolean setName(String name) {
    this.name = name;
    return true;
  }

  @Implementation
  protected boolean setScanMode(int scanMode) {
    if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE
        && scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
        && scanMode != BluetoothAdapter.SCAN_MODE_NONE) {
      return false;
    }

    this.scanMode = scanMode;
    return true;
  }

  @Implementation
  protected int getScanMode() {
    return scanMode;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected boolean isMultipleAdvertisementSupported() {
    return isMultipleAdvertisementSupported;
  }

  /**
   * Validate a Bluetooth address, such as "00:43:A8:23:10:F0" Alphabetic characters must be
   * uppercase to be valid.
   *
   * @param address Bluetooth address as string
   * @return true if the address is valid, false otherwise
   */
  @Implementation
  protected static boolean checkBluetoothAddress(String address) {
    if (address == null || address.length() != ADDRESS_LENGTH) {
      return false;
    }
    for (int i = 0; i < ADDRESS_LENGTH; i++) {
      char c = address.charAt(i);
      switch (i % 3) {
        case 0:
        case 1:
          if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) {
            // hex character, OK
            break;
          }
          return false;
        case 2:
          if (c == ':') {
            break; // OK
          }
          return false;
      }
    }
    return true;
  }

  /**
   * Returns the connection state for the given Bluetooth {@code profile}, defaulting to {@link
   * BluetoothProfile.STATE_DISCONNECTED} if the profile's connection state was never set.
   *
   * <p>Set a Bluetooth profile's connection state via {@link #setProfileConnectionState(int, int)}.
   */
  @Implementation
  protected int getProfileConnectionState(int profile) {
    Integer state = profileConnectionStateData.get(profile);
    if (state == null) {
      return BluetoothProfile.STATE_DISCONNECTED;
    }
    return state;
  }

  public void setAddress(String address) {
    this.address = address;
  }

  public void setState(int state) {
    this.state = state;
  }

  public void setEnabled(boolean enabled) {
    this.enabled = enabled;
  }

  public void setIsMultipleAdvertisementSupported(boolean supported) {
    isMultipleAdvertisementSupported = supported;
  }

  /** Sets the connection state {@code state} for the given BluetoothProfile {@code profile} */
  public void setProfileConnectionState(int profile, int state) {
    profileConnectionStateData.put(profile, state);
  }

  /**
   * Sets the active BluetoothProfile {@code proxy} for the given {@code profile}. Will always
   * affect behavior of {@link BluetoothAdapter#getProfileProxy} and {@link
   * BluetoothAdapter#closeProfileProxy}. Call to {@link BluetoothAdapter#closeProfileProxy} can
   * remove the set active proxy.
   *
   * @param proxy can be 'null' to simulate the situation where {@link
   *     BluetoothAdapter#getProfileProxy} would return 'false'. This can happen on older Android
   *     versions for Bluetooth profiles introduced in later Android versions.
   */
  public void setProfileProxy(int profile, @Nullable BluetoothProfile proxy) {
    isOverridingProxyBehavior = true;
    if (proxy != null) {
      profileProxies.put(profile, proxy);
    }
  }

  /**
   * @return 'true' if active (non-null) proxy has been set by {@link
   *     ShadowBluetoothAdapter#setProfileProxy} for the given {@code profile} AND it has not been
   *     "deactivated" by a call to {@link BluetoothAdapter#closeProfileProxy}. Only meaningful if
   *     {@link ShadowBluetoothAdapter#setProfileProxy} has been previously called.
   */
  public boolean hasActiveProfileProxy(int profile) {
    return profileProxies.get(profile) != null;
  }

  /**
   * Overrides behavior of {@link getProfileProxy} if {@link ShadowBluetoothAdapter#setProfileProxy}
   * has been previously called.
   *
   * If active (non-null) proxy has been set by {@link setProfileProxy} for the given {@code
   * profile}, {@link getProfileProxy} will immediately call {@code onServiceConnected} of the given
   * BluetoothProfile.ServiceListener {@code listener}.
   *
   * @return 'true' if a proxy object has been set by {@link setProfileProxy} for the given
   *     BluetoothProfile {@code profile}
   */
  @Implementation
  protected boolean getProfileProxy(
      Context context, BluetoothProfile.ServiceListener listener, int profile) {
    if (!isOverridingProxyBehavior) {
      return directlyOn(realAdapter, BluetoothAdapter.class)
          .getProfileProxy(context, listener, profile);
    }

    BluetoothProfile proxy = profileProxies.get(profile);
    if (proxy == null) {
      return false;
    } else {
      listener.onServiceConnected(profile, proxy);
      return true;
    }
  }

  /**
   * Overrides behavior of {@link closeProfileProxy} if {@link
   * ShadowBluetoothAdapter#setProfileProxy} has been previously called.
   *
   * If the given non-null BluetoothProfile {@code proxy} was previously set for the given {@code
   * profile} by {@link ShadowBluetoothAdapter#setProfileProxy}, this proxy will be "deactivated".
   */
  @Implementation
  protected void closeProfileProxy(int profile, BluetoothProfile proxy) {
    if (!isOverridingProxyBehavior) {
      directlyOn(realAdapter, BluetoothAdapter.class).closeProfileProxy(profile, proxy);
      return;
    }

    if (proxy != null && proxy.equals(profileProxies.get(profile))) {
      profileProxies.remove(profile);
    }
  }
}