package org.robolectric.shadows;

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.N;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import static org.robolectric.util.ReflectionHelpers.getField;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.IWindowFocusObserver;
import android.view.IWindowId;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.WindowId;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import java.io.PrintStream;
import java.lang.reflect.Method;
import org.robolectric.android.AccessibilityUtil;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.TimeUtils;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

@Implements(View.class)
@SuppressLint("NewApi")
public class ShadowView {

  @RealObject
  protected View realView;

  private View.OnClickListener onClickListener;
  private View.OnLongClickListener onLongClickListener;
  private View.OnFocusChangeListener onFocusChangeListener;
  private View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener;
  private boolean wasInvalidated;
  private View.OnTouchListener onTouchListener;
  protected AttributeSet attributeSet;
  public Point scrollToCoordinates = new Point();
  private boolean didRequestLayout;
  private MotionEvent lastTouchEvent;
  private float scaleX = 1.0f;
  private float scaleY = 1.0f;
  private int hapticFeedbackPerformed = -1;
  private boolean onLayoutWasCalled;
  private View.OnCreateContextMenuListener onCreateContextMenuListener;
  private Rect globalVisibleRect;
  private int layerType;

  /**
   * Calls {@code performClick()} on a {@code View} after ensuring that it and its ancestors are visible and that it
   * is enabled.
   *
   * @param view the view to click on
   * @return true if {@code View.OnClickListener}s were found and fired, false otherwise.
   * @throws RuntimeException if the preconditions are not met.
   * @deprecated Please use Espresso for view interactions
   */
  @Deprecated
  public static boolean clickOn(View view) {
    ShadowView shadowView = Shadow.extract(view);
    return shadowView.checkedPerformClick();
  }

  /**
   * Returns a textual representation of the appearance of the object.
   *
   * @param view the view to visualize
   * @return Textual representation of the appearance of the object.
   */
  public static String visualize(View view) {
    Canvas canvas = new Canvas();
    view.draw(canvas);
    ShadowCanvas shadowCanvas = Shadow.extract(canvas);
    return shadowCanvas.getDescription();
  }

  /**
   * Emits an xml-like representation of the view to System.out.
   *
   * @param view the view to dump.
   * @deprecated - Please use {@link androidx.test.espresso.util.HumanReadables#describe(View)}
   */
  @SuppressWarnings("UnusedDeclaration")
  @Deprecated
  public static void dump(View view) {
    ShadowView shadowView = Shadow.extract(view);
    shadowView.dump();
  }

  /**
   * Returns the text contained within this view.
   *
   * @param view the view to scan for text
   * @return Text contained within this view.
   */
  @SuppressWarnings("UnusedDeclaration")
  public static String innerText(View view) {
    ShadowView shadowView = Shadow.extract(view);
    return shadowView.innerText();
  }

  @Implementation
  protected void __constructor__(Context context, AttributeSet attributeSet, int defStyle) {
    if (context == null) throw new NullPointerException("no context");
    this.attributeSet = attributeSet;
    invokeConstructor(View.class, realView,
        ClassParameter.from(Context.class, context),
        ClassParameter.from(AttributeSet.class, attributeSet),
        ClassParameter.from(int.class, defStyle));
  }

  @Implementation
  protected void setLayerType(int layerType, Paint paint) {
    this.layerType = layerType;
  }

  @Implementation
  protected void setOnFocusChangeListener(View.OnFocusChangeListener l) {
    onFocusChangeListener = l;
    directly().setOnFocusChangeListener(l);
  }

  @Implementation
  protected void setOnClickListener(View.OnClickListener onClickListener) {
    this.onClickListener = onClickListener;
    directly().setOnClickListener(onClickListener);
  }

  @Implementation
  protected void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
    this.onLongClickListener = onLongClickListener;
    directly().setOnLongClickListener(onLongClickListener);
  }

  @Implementation
  protected void setOnSystemUiVisibilityChangeListener(
      View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener) {
    this.onSystemUiVisibilityChangeListener = onSystemUiVisibilityChangeListener;
    directly().setOnSystemUiVisibilityChangeListener(onSystemUiVisibilityChangeListener);
  }

  @Implementation
  protected void setOnCreateContextMenuListener(
      View.OnCreateContextMenuListener onCreateContextMenuListener) {
    this.onCreateContextMenuListener = onCreateContextMenuListener;
    directly().setOnCreateContextMenuListener(onCreateContextMenuListener);
  }

  @Implementation
  protected void draw(android.graphics.Canvas canvas) {
    Drawable background = realView.getBackground();
    if (background != null) {
      ShadowCanvas shadowCanvas = Shadow.extract(canvas);
      shadowCanvas.appendDescription("background:");
      background.draw(canvas);
    }
  }

  @Implementation
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    onLayoutWasCalled = true;
    directlyOn(realView, View.class, "onLayout",
        ClassParameter.from(boolean.class, changed),
        ClassParameter.from(int.class, left),
        ClassParameter.from(int.class, top),
        ClassParameter.from(int.class, right),
        ClassParameter.from(int.class, bottom));
  }

  public boolean onLayoutWasCalled() {
    return onLayoutWasCalled;
  }

  @Implementation
  protected void requestLayout() {
    didRequestLayout = true;
    directly().requestLayout();
  }

  public boolean didRequestLayout() {
    return didRequestLayout;
  }

  public void setDidRequestLayout(boolean didRequestLayout) {
    this.didRequestLayout = didRequestLayout;
  }

  public void setViewFocus(boolean hasFocus) {
    if (onFocusChangeListener != null) {
      onFocusChangeListener.onFocusChange(realView, hasFocus);
    }
  }

  @Implementation
  protected void invalidate() {
    wasInvalidated = true;
    directly().invalidate();
  }

  @Implementation
  protected boolean onTouchEvent(MotionEvent event) {
    lastTouchEvent = event;
    return directly().onTouchEvent(event);
  }

  @Implementation
  protected void setOnTouchListener(View.OnTouchListener onTouchListener) {
    this.onTouchListener = onTouchListener;
    directly().setOnTouchListener(onTouchListener);
  }

  public MotionEvent getLastTouchEvent() {
    return lastTouchEvent;
  }

  /**
   * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string.
   *
   * Robolectric extension.
   * @return String representation of this view.
   */
  public String innerText() {
    return "";
  }

  /**
   * Dumps the status of this {@code View} to {@code System.out}
   * @deprecated - Please use {@link androidx.test.espresso.util.HumanReadables#describe(View)}
   */
  @Deprecated
  public void dump() {
    dump(System.out, 0);
  }

  /**
   * Dumps the status of this {@code View} to {@code System.out} at the given indentation level
   * @param out Output stream.
   * @param indent Indentation level.
   * @deprecated - Please use {@link androidx.test.espresso.util.HumanReadables#describe(View)}
   */
  @Deprecated
  public void dump(PrintStream out, int indent) {
    dumpFirstPart(out, indent);
    out.println("/>");
  }

  @Deprecated
  protected void dumpFirstPart(PrintStream out, int indent) {
    dumpIndent(out, indent);

    out.print("<" + realView.getClass().getSimpleName());
    dumpAttributes(out);
  }

  @Deprecated
  protected void dumpAttributes(PrintStream out) {
    if (realView.getId() > 0) {
      dumpAttribute(out, "id", realView.getContext().getResources().getResourceName(realView.getId()));
    }

    switch (realView.getVisibility()) {
      case View.VISIBLE:
        break;
      case View.INVISIBLE:
        dumpAttribute(out, "visibility", "INVISIBLE");
        break;
      case View.GONE:
        dumpAttribute(out, "visibility", "GONE");
        break;
    }
  }

  @Deprecated
  protected void dumpAttribute(PrintStream out, String name, String value) {
    out.print(" " + name + "=\"" + (value == null ? null : TextUtils.htmlEncode(value)) + "\"");
  }

  @Deprecated
  protected void dumpIndent(PrintStream out, int indent) {
    for (int i = 0; i < indent; i++) out.print(" ");
  }

  /**
   * @return whether or not {@link #invalidate()} has been called
   */
  public boolean wasInvalidated() {
    return wasInvalidated;
  }

  /**
   * Clears the wasInvalidated flag
   */
  public void clearWasInvalidated() {
    wasInvalidated = false;
  }

  /**
   * Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app.
   *
   * If running with LooperMode PAUSED will also idle the main Looper.
   *
   * @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible.
   * @return Return value of the underlying click operation.
   * @deprecated - Please use Espresso for View interactions.
   */
  @Deprecated
  public boolean checkedPerformClick() {
    if (!realView.isShown()) {
      throw new RuntimeException("View is not visible and cannot be clicked");
    }
    if (!realView.isEnabled()) {
      throw new RuntimeException("View is not enabled and cannot be clicked");
    }

    AccessibilityUtil.checkViewIfCheckingEnabled(realView);
    boolean res = realView.performClick();
    shadowMainLooper().idleIfPaused();
    return res;
  }

  /**
   * @return Touch listener, if set.
   */
  public View.OnTouchListener getOnTouchListener() {
    return onTouchListener;
  }

  /**
   * @return Returns click listener, if set.
   */
  public View.OnClickListener getOnClickListener() {
    return onClickListener;
  }

  /**
   * @return Returns long click listener, if set.
   */
  public View.OnLongClickListener getOnLongClickListener() {
    return onLongClickListener;
  }

  /**
   * @return Returns system ui visibility change listener.
   */
  public View.OnSystemUiVisibilityChangeListener getOnSystemUiVisibilityChangeListener() {
    return onSystemUiVisibilityChangeListener;
  }

  /**
   * @return Returns create ContextMenu listener, if set.
   */
  public View.OnCreateContextMenuListener getOnCreateContextMenuListener() {
    return onCreateContextMenuListener;
  }

  // @Implementation
  // protected Bitmap getDrawingCache() {
  //   return ReflectionHelpers.callConstructor(Bitmap.class);
  // }

  @Implementation
  protected boolean post(Runnable action) {
    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
      return directly().post(action);
    } else {
      ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
      return true;
    }
  }

  @Implementation
  protected boolean postDelayed(Runnable action, long delayMills) {
    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
      return directly().postDelayed(action, delayMills);
    } else {
      ShadowApplication.getInstance()
          .getForegroundThreadScheduler()
          .postDelayed(action, delayMills);
      return true;
    }
  }

  @Implementation
  protected void postInvalidateDelayed(long delayMilliseconds) {
    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
      directly().postInvalidateDelayed(delayMilliseconds);
    } else {
      ShadowApplication.getInstance()
          .getForegroundThreadScheduler()
          .postDelayed(
              new Runnable() {
                @Override
                public void run() {
                  realView.invalidate();
                }
              },
              delayMilliseconds);
    }
  }

  @Implementation
  protected boolean removeCallbacks(Runnable callback) {
    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
      return directlyOn(realView, View.class).removeCallbacks(callback);
    } else {
      ShadowLegacyLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
      shadowLooper.getScheduler().remove(callback);
      return true;
    }
  }

  @Implementation
  protected void scrollTo(int x, int y) {
    try {
      Method method = View.class.getDeclaredMethod("onScrollChanged", new Class[]{int.class, int.class, int.class, int.class});
      method.setAccessible(true);
      method.invoke(realView, x, y, scrollToCoordinates.x, scrollToCoordinates.y);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    scrollToCoordinates = new Point(x, y);
    ReflectionHelpers.setField(realView, "mScrollX", x);
    ReflectionHelpers.setField(realView, "mScrollY", y);
  }

  @Implementation
  protected void scrollBy(int x, int y) {
    scrollTo(getScrollX() + x, getScrollY() + y);
  }

  @Implementation
  protected int getScrollX() {
    return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
  }

  @Implementation
  protected int getScrollY() {
    return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
  }

  @Implementation
  protected void setScrollX(int scrollX) {
    scrollTo(scrollX, scrollToCoordinates.y);
  }

  @Implementation
  protected void setScrollY(int scrollY) {
    scrollTo(scrollToCoordinates.x, scrollY);
  }

  @Implementation
  protected int getLayerType() {
    return this.layerType;
  }

  @Implementation
  protected void setAnimation(final Animation animation) {
    directly().setAnimation(animation);

    if (animation != null) {
      new AnimationRunner(animation);
    }
  }

  private AnimationRunner animationRunner;

  private class AnimationRunner implements Runnable {
    private final Animation animation;
    private long startTime, startOffset, elapsedTime;

    AnimationRunner(Animation animation) {
      this.animation = animation;
      start();
    }

    private void start() {
      startTime = animation.getStartTime();
      startOffset = animation.getStartOffset();
      Choreographer choreographer = Choreographer.getInstance();
      if (animationRunner != null) {
        choreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, animationRunner, null);
      }
      animationRunner = this;
      int startDelay;
      if (startTime == Animation.START_ON_FIRST_FRAME) {
        startDelay = (int) startOffset;
      } else {
        startDelay = (int) ((startTime + startOffset) - SystemClock.uptimeMillis());
      }
      choreographer.postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, this, null, startDelay);
    }

    @Override
    public void run() {
      // Abort if start time has been messed with, as this simulation is only designed to handle
      // standard situations.
      if ((animation.getStartTime() == startTime && animation.getStartOffset() == startOffset) &&
          animation.getTransformation(startTime == Animation.START_ON_FIRST_FRAME ?
              SystemClock.uptimeMillis() : (startTime + startOffset + elapsedTime), new Transformation()) &&
              // We can't handle infinitely repeating animations in the current scheduling model,
              // so abort after one iteration.
              !(animation.getRepeatCount() == Animation.INFINITE && elapsedTime >= animation.getDuration())) {
        // Update startTime if it had a value of Animation.START_ON_FIRST_FRAME
        startTime = animation.getStartTime();
        // TODO: get the correct value for ShadowPausedLooper mode
        elapsedTime += ShadowChoreographer.getFrameInterval() / TimeUtils.NANOS_PER_MS;
        Choreographer.getInstance().postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
      } else {
        animationRunner = null;
      }
    }
  }

  @Implementation(minSdk = KITKAT)
  protected boolean isAttachedToWindow() {
    return getAttachInfo() != null;
  }

  private Object getAttachInfo() {
    return reflector(_View_.class, realView).getAttachInfo();
  }

  /** Accessor interface for {@link View}'s internals. */
  @ForType(View.class)
  private interface _View_ {
    @Accessor("mAttachInfo")
    Object getAttachInfo();

    void onAttachedToWindow();

    void onDetachedFromWindow();
  }

  public void callOnAttachedToWindow() {
    reflector(_View_.class, realView).onAttachedToWindow();
  }

  public void callOnDetachedFromWindow() {
    reflector(_View_.class, realView).onDetachedFromWindow();
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected WindowId getWindowId() {
    return WindowIdHelper.getWindowId(this);
  }

  @Implementation
  protected boolean performHapticFeedback(int hapticFeedbackType) {
    hapticFeedbackPerformed = hapticFeedbackType;
    return true;
  }

  @Implementation
  protected boolean getGlobalVisibleRect(Rect rect, Point globalOffset) {
    if (globalVisibleRect == null) {
      return directly().getGlobalVisibleRect(rect, globalOffset);
    }

    if (!globalVisibleRect.isEmpty()) {
      rect.set(globalVisibleRect);
      if (globalOffset != null) {
        rect.offset(-globalOffset.x, -globalOffset.y);
      }
      return true;
    }
    rect.setEmpty();
    return false;
  }

  public void setGlobalVisibleRect(Rect rect) {
    if (rect != null) {
      globalVisibleRect = new Rect();
      globalVisibleRect.set(rect);
    } else {
      globalVisibleRect = null;
    }
  }

  public int lastHapticFeedbackPerformed() {
    return hapticFeedbackPerformed;
  }

  public void setMyParent(ViewParent viewParent) {
    directlyOn(realView, View.class, "assignParent", ClassParameter.from(ViewParent.class, viewParent));
  }

  @Implementation
  protected void getWindowVisibleDisplayFrame(Rect outRect) {
    // TODO: figure out how to simulate this logic instead
    // if (mAttachInfo != null) {
    //   mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);

    ShadowDisplay.getDefaultDisplay().getRectSize(outRect);
  }

  @Implementation(minSdk = N)
  protected void getWindowDisplayFrame(Rect outRect) {
    // TODO: figure out how to simulate this logic instead
    // if (mAttachInfo != null) {
    //   mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);

    ShadowDisplay.getDefaultDisplay().getRectSize(outRect);
  }

  private View directly() {
    return directlyOn(realView, View.class);
  }

  public static class WindowIdHelper {
    public static WindowId getWindowId(ShadowView shadowView) {
      if (shadowView.isAttachedToWindow()) {
        Object attachInfo = shadowView.getAttachInfo();
        if (getField(attachInfo, "mWindowId") == null) {
          IWindowId iWindowId = new MyIWindowIdStub();
          reflector(_AttachInfo_.class, attachInfo).setWindowId(new WindowId(iWindowId));
          reflector(_AttachInfo_.class, attachInfo).setIWindowId(iWindowId);
        }
      }

      return shadowView.directly().getWindowId();
    }

    private static class MyIWindowIdStub extends IWindowId.Stub {
      @Override
      public void registerFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
      }

      @Override
      public void unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
      }

      @Override
      public boolean isFocused() throws RemoteException {
        return true;
      }
    }
  }

  /** Accessor interface for android.view.View.AttachInfo's internals. */
  @ForType(className = "android.view.View$AttachInfo")
  interface _AttachInfo_ {

    @Accessor("mIWindowId")
    void setIWindowId(IWindowId iWindowId);

    @Accessor("mWindowId")
    void setWindowId(WindowId windowId);
  }
}


package org.robolectric.internal.bytecode;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.objectweb.asm.tree.MethodInsnNode;
import org.robolectric.annotation.internal.DoNotInstrument;
import org.robolectric.annotation.internal.Instrument;
import org.robolectric.shadow.api.Shadow;

/**
 * Configuration rules for {@link SandboxClassLoader}.
 */
public class InstrumentationConfiguration {

  public static Builder newBuilder() {
    return new Builder();
  }

  static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet(
      RobolectricInternals.class.getName(),
      InvokeDynamicSupport.class.getName(),
      Shadow.class.getName(),

      // these classes are deprecated and will be removed soon:
      "org.robolectric.util.FragmentTestUtil",
      "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity"
  );

  static final Set<String> RESOURCES_TO_ALWAYS_ACQUIRE = Sets.newHashSet("build.prop");

  private final List<String> instrumentedPackages;
  private final Set<String> instrumentedClasses;
  private final Set<String> classesToNotInstrument;
  private final Map<String, String> classNameTranslations;
  private final Set<MethodRef> interceptedMethods;
  private final Set<String> classesToNotAcquire;
  private final Set<String> packagesToNotAcquire;
  private final Set<String> packagesToNotInstrument;
  private int cachedHashCode;

  private final TypeMapper typeMapper;
  private final Set<MethodRef> methodsToIntercept;

  protected InstrumentationConfiguration(
      Map<String, String> classNameTranslations,
      Collection<MethodRef> interceptedMethods,
      Collection<String> instrumentedPackages,
      Collection<String> instrumentedClasses,
      Collection<String> classesToNotAcquire,
      Collection<String> packagesToNotAquire,
      Collection<String> classesToNotInstrument,
      Collection<String> packagesToNotInstrument) {
    this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations);
    this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods);
    this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages);
    this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses);
    this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire);
    this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire);
    this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument);
    this.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument);
    this.cachedHashCode = 0;

    this.typeMapper = new TypeMapper(classNameTranslations());
    this.methodsToIntercept = ImmutableSet.copyOf(convertToSlashes(methodsToIntercept()));
  }

  /**
   * Determine if {@link SandboxClassLoader} should instrument a given class.
   *
   * @param   mutableClass The class to check.
   * @return  True if the class should be instrumented.
   */
  public boolean shouldInstrument(MutableClass mutableClass) {
    return !(mutableClass.isInterface()
            || mutableClass.isAnnotation()
            || mutableClass.hasAnnotation(DoNotInstrument.class))
        && (isInInstrumentedPackage(mutableClass.getName())
            || instrumentedClasses.contains(mutableClass.getName())
            || mutableClass.hasAnnotation(Instrument.class))
        && !(classesToNotInstrument.contains(mutableClass.getName()))
        && !(isInPackagesToNotInstrument(mutableClass.getName()));
  }

  /**
   * Determine if {@link SandboxClassLoader} should load a given class.
   *
   * @param   name The fully-qualified class name.
   * @return  True if the class should be loaded.
   */
  public boolean shouldAcquire(String name) {
    if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) {
      return true;
    }

    if (name.equals("java.util.jar.StrictJarFile")) {
      return true;
    }

    // android.R and com.android.internal.R classes must be loaded from the framework jar
    if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) {
      return true;
    }

    // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
    if (name.equals("javax.net.ssl.DistinguishedNameParser")
        || name.equals("javax.microedition.khronos.opengles.GL")) {
      return true;
    }

    for (String packageName : packagesToNotAcquire) {
      if (name.startsWith(packageName)) return false;
    }

    // R classes must be loaded from system CP
    boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$");
    return !isRClass && !classesToNotAcquire.contains(name);
  }

  /**
   * Determine if {@link SandboxClassLoader} should load a given resource.
   *
   * @param name The fully-qualified resource name.
   * @return True if the resource should be loaded.
   */
  public boolean shouldAcquireResource(String name) {
    return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name);
  }

  public Set<MethodRef> methodsToIntercept() {
    return Collections.unmodifiableSet(interceptedMethods);
  }

  /**
   * Map from a requested class to an alternate stand-in, or not.
   *
   * @return Mapping of class name translations.
   */
  public Map<String, String> classNameTranslations() {
    return Collections.unmodifiableMap(classNameTranslations);
  }

  public boolean containsStubs(String className) {
    return className.startsWith("com.google.android.maps.");
  }

  private boolean isInInstrumentedPackage(String className) {
    for (String instrumentedPackage : instrumentedPackages) {
      if (className.startsWith(instrumentedPackage)) {
        return true;
      }
    }
    return false;
  }

  private boolean isInPackagesToNotInstrument(String className) {
    for (String notInstrumentedPackage : packagesToNotInstrument) {
      if (className.startsWith(notInstrumentedPackage)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    InstrumentationConfiguration that = (InstrumentationConfiguration) o;

    if (!classNameTranslations.equals(that.classNameTranslations)) return false;
    if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false;
    if (!instrumentedPackages.equals(that.instrumentedPackages)) return false;
    if (!instrumentedClasses.equals(that.instrumentedClasses)) return false;
    if (!interceptedMethods.equals(that.interceptedMethods)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    if (cachedHashCode != 0) {
      return cachedHashCode;
    }

    int result = instrumentedPackages.hashCode();
    result = 31 * result + instrumentedClasses.hashCode();
    result = 31 * result + classNameTranslations.hashCode();
    result = 31 * result + interceptedMethods.hashCode();
    result = 31 * result + classesToNotAcquire.hashCode();
    cachedHashCode = result;
    return result;
  }

  public String remapParamType(String desc) {
    return typeMapper.remapParamType(desc);
  }

  public String remapParams(String desc) {
    return typeMapper.remapParams(desc);
  }

  public String mappedTypeName(String internalName) {
    return typeMapper.mappedTypeName(internalName);
  }

  boolean shouldIntercept(MethodInsnNode targetMethod) {
    if (targetMethod.name.equals("<init>")) return false; // sorry, can't strip out calls to super() in constructor
    return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
        || methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
  }

  private static Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) {
    HashSet<MethodRef> transformed = new HashSet<>();
    for (MethodRef methodRef : methodRefs) {
      transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
    }
    return transformed;
  }

  private static String internalize(String className) {
    return className.replace('.', '/');
  }

  public static final class Builder {
    public final Collection<String> instrumentedPackages = new HashSet<>();
    public final Collection<MethodRef> interceptedMethods = new HashSet<>();
    public final Map<String, String> classNameTranslations = new HashMap<>();
    public final Collection<String> classesToNotAcquire = new HashSet<>();
    public final Collection<String> packagesToNotAcquire = new HashSet<>();
    public final Collection<String> instrumentedClasses = new HashSet<>();
    public final Collection<String> classesToNotInstrument = new HashSet<>();
    public final Collection<String> packagesToNotInstrument = new HashSet<>();

    public Builder() {
    }

    public Builder(InstrumentationConfiguration classLoaderConfig) {
      instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages);
      interceptedMethods.addAll(classLoaderConfig.interceptedMethods);
      classNameTranslations.putAll(classLoaderConfig.classNameTranslations);
      classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire);
      packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire);
      instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses);
      classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument);
      packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument);
    }

    public Builder doNotAcquireClass(Class<?> clazz) {
      doNotAcquireClass(clazz.getName());
      return this;
    }

    public Builder doNotAcquireClass(String className) {
      this.classesToNotAcquire.add(className);
      return this;
    }

    public Builder doNotAcquirePackage(String packageName) {
      this.packagesToNotAcquire.add(packageName);
      return this;
    }

    public Builder addClassNameTranslation(String fromName, String toName) {
      classNameTranslations.put(fromName, toName);
      return this;
    }

    public Builder addInterceptedMethod(MethodRef methodReference) {
      interceptedMethods.add(methodReference);
      return this;
    }

    public Builder addInstrumentedClass(String name) {
      instrumentedClasses.add(name);
      return this;
    }

    public Builder addInstrumentedPackage(String packageName) {
      instrumentedPackages.add(packageName);
      return this;
    }

    public Builder doNotInstrumentClass(String className) {
      this.classesToNotInstrument.add(className);
      return this;
    }

    public Builder doNotInstrumentPackage(String packageName) {
      this.packagesToNotInstrument.add(packageName);
      return this;
    }

    public InstrumentationConfiguration build() {
      return new InstrumentationConfiguration(
          classNameTranslations,
          interceptedMethods,
          instrumentedPackages,
          instrumentedClasses,
          classesToNotAcquire,
          packagesToNotAcquire,
          classesToNotInstrument,
          packagesToNotInstrument);
    }
  }
}


package org.robolectric.shadows;

import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.P;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.robolectric.shadow.api.Shadow.directlyOn;

import android.app.Activity;
import android.app.ActivityThread;
import android.app.Fragment;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityResult;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.Intent.FilterComparison;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.util.Pair;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivity.IntentForResult;
import org.robolectric.shadows.ShadowApplication.Wrapper;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;

@Implements(value = Instrumentation.class, looseSignatures = true)
public class ShadowInstrumentation {

  @RealObject private Instrumentation realObject;

  private List<Intent> startedActivities = new ArrayList<>();
  private List<IntentForResult> startedActivitiesForResults = new ArrayList<>();
  private Map<FilterComparison, Integer> intentRequestCodeMap = new HashMap<>();
  private List<Intent.FilterComparison> startedServices = new ArrayList<>();
  private List<Intent.FilterComparison> stoppedServices = new ArrayList<>();
  private List<Intent> broadcastIntents = new ArrayList<>();
  private List<ServiceConnection> boundServiceConnections =
      Collections.synchronizedList(new ArrayList<>());
  private List<ServiceConnection> unboundServiceConnections =
      Collections.synchronizedList(new ArrayList<>());
  private List<Wrapper> registeredReceivers = new ArrayList<>();
  // map of pid+uid to granted permissions
  private final Map<Pair<Integer, Integer>, Set<String>> grantedPermissionsMap = new HashMap<>();
  private boolean unbindServiceShouldThrowIllegalArgument = false;
  private Map<Intent.FilterComparison, ServiceConnectionDataWrapper>
      serviceConnectionDataForIntent = new HashMap<>();
  // default values for bindService
  private ServiceConnectionDataWrapper defaultServiceConnectionData =
      new ServiceConnectionDataWrapper(null, null);
  private List<String> unbindableActions = new ArrayList<>();
  private Map<String, Intent> stickyIntents = new LinkedHashMap<>();
  private Handler mainHandler;
  private Map<ServiceConnection, ServiceConnectionDataWrapper>
      serviceConnectionDataForServiceConnection = new HashMap<>();

  private boolean checkActivities;

  @Implementation(minSdk = P)
  protected Activity startActivitySync(Intent intent, Bundle options) {
    throw new UnsupportedOperationException("Implement me!!");
  }

  @Implementation
  protected ActivityResult execStartActivity(
      Context who,
      IBinder contextThread,
      IBinder token,
      Activity target,
      Intent intent,
      int requestCode,
      Bundle options) {

    verifyActivityInManifest(intent);
    logStartedActivity(intent, requestCode, options);

    if (who == null) {
      return null;
    }
    return directlyOn(realObject, Instrumentation.class)
        .execStartActivity(who, contextThread, token, target, intent, requestCode, options);
  }

  @Implementation(maxSdk = LOLLIPOP_MR1)
  protected ActivityResult execStartActivity(
      Context who,
      IBinder contextThread,
      IBinder token,
      Fragment target,
      Intent intent,
      int requestCode,
      Bundle options) {
    verifyActivityInManifest(intent);
    logStartedActivity(intent, requestCode, options);
    return null;
  }

  private void logStartedActivity(Intent intent, int requestCode, Bundle options) {
    startedActivities.add(intent);
    intentRequestCodeMap.put(new FilterComparison(intent), requestCode);
    startedActivitiesForResults.add(new IntentForResult(intent, requestCode, options));
  }

  private void verifyActivityInManifest(Intent intent) {
    if (checkActivities
        && RuntimeEnvironment.application
                .getPackageManager()
                .resolveActivity(intent, MATCH_DEFAULT_ONLY)
            == null) {
      throw new ActivityNotFoundException(intent.getAction());
    }
  }

  @Implementation
  protected void execStartActivities(
      Context who,
      IBinder contextThread,
      IBinder token,
      Activity target,
      Intent[] intents,
      Bundle options) {
    for (Intent intent : intents) {
      execStartActivity(who, contextThread, token, target, intent, -1, options);
    }
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void execStartActivityFromAppTask(
      Context who, IBinder contextThread, Object appTask, Intent intent, Bundle options) {
    throw new UnsupportedOperationException("Implement me!!");
  }

  @Implementation(minSdk = M)
  protected ActivityResult execStartActivity(
      Context who,
      IBinder contextThread,
      IBinder token,
      String target,
      Intent intent,
      int requestCode,
      Bundle options) {
    verifyActivityInManifest(intent);
    logStartedActivity(intent, requestCode, options);

    return directlyOn(realObject, Instrumentation.class)
        .execStartActivity(who, contextThread, token, target, intent, requestCode, options);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected ActivityResult execStartActivity(
      Context who,
      IBinder contextThread,
      IBinder token,
      String resultWho,
      Intent intent,
      int requestCode,
      Bundle options,
      UserHandle user) {
    throw new UnsupportedOperationException("Implement me!!");
  }

  @Implementation(minSdk = M, maxSdk = P)
  protected ActivityResult execStartActivityAsCaller(
      Context who,
      IBinder contextThread,
      IBinder token,
      Activity target,
      Intent intent,
      int requestCode,
      Bundle options,
      boolean ignoreTargetSecurity,
      int userId) {
    throw new UnsupportedOperationException("Implement me!!");
  }

  void sendOrderedBroadcast(
      Intent intent,
      String receiverPermission,
      BroadcastReceiver resultReceiver,
      Handler scheduler,
      int initialCode,
      String initialData,
      Bundle initialExtras,
      Context context) {
    List<Wrapper> receivers = getAppropriateWrappers(intent, receiverPermission);
    sortByPriority(receivers);
    receivers.add(new Wrapper(resultReceiver, null, context, null, scheduler));
    postOrderedToWrappers(receivers, intent, initialCode, initialData, initialExtras, context);
  }

  void assertNoBroadcastListenersOfActionRegistered(ContextWrapper context, String action) {
    for (Wrapper registeredReceiver : registeredReceivers) {
      if (registeredReceiver.context == context.getBaseContext()) {
        Iterator<String> actions = registeredReceiver.intentFilter.actionsIterator();
        while (actions.hasNext()) {
          if (actions.next().equals(action)) {
            RuntimeException e =
                new IllegalStateException(
                    "Unexpected BroadcastReceiver on "
                        + context
                        + " with action "
                        + action
                        + " "
                        + registeredReceiver.broadcastReceiver
                        + " that was originally registered here:");
            e.setStackTrace(registeredReceiver.exception.getStackTrace());
            throw e;
          }
        }
      }
    }
  }

  /** Returns the BroadcaseReceivers wrappers, matching intent's action and permissions. */
  private List<Wrapper> getAppropriateWrappers(Intent intent, String receiverPermission) {
    broadcastIntents.add(intent);

    List<Wrapper> result = new ArrayList<>();

    List<Wrapper> copy = new ArrayList<>();
    copy.addAll(registeredReceivers);
    String intentClass =
        intent.getComponent() != null ? intent.getComponent().getClassName() : null;
    for (Wrapper wrapper : copy) {
      if ((hasMatchingPermission(wrapper.broadcastPermission, receiverPermission)
              && wrapper.intentFilter.matchAction(intent.getAction()))
          || (intentClass != null
              && intentClass.equals(wrapper.broadcastReceiver.getClass().getName()))) {
        final int match =
            wrapper.intentFilter.matchData(intent.getType(), intent.getScheme(), intent.getData());
        if (match != IntentFilter.NO_MATCH_DATA && match != IntentFilter.NO_MATCH_TYPE) {
          result.add(wrapper);
        }
      }
    }
    return result;
  }

  private void postIntent(
      Intent intent, Wrapper wrapper, final AtomicBoolean abort, Context context, int resultCode) {
    final Handler scheduler =
        (wrapper.scheduler != null) ? wrapper.scheduler : getMainHandler(context);
    final BroadcastReceiver receiver = wrapper.broadcastReceiver;
    final ShadowBroadcastReceiver shReceiver = Shadow.extract(receiver);
    final Intent broadcastIntent = intent;
    scheduler.post(
        new Runnable() {
          @Override
          public void run() {
            receiver.setPendingResult(
                ShadowBroadcastPendingResult.create(resultCode, null, null, false));
            shReceiver.onReceive(context, broadcastIntent, abort);
          }
        });
  }

  private void postToWrappers(
      List<Wrapper> wrappers, Intent intent, Context context, int resultCode) {
    AtomicBoolean abort =
        new AtomicBoolean(false); // abort state is shared among all broadcast receivers
    for (Wrapper wrapper : wrappers) {
      postIntent(intent, wrapper, abort, context, resultCode);
    }
  }

  private void postOrderedToWrappers(
      List<Wrapper> wrappers,
      final Intent intent,
      int initialCode,
      String data,
      Bundle extras,
      final Context context) {
    final AtomicBoolean abort =
        new AtomicBoolean(false); // abort state is shared among all broadcast receivers
    ListenableFuture<BroadcastResultHolder> future =
        immediateFuture(new BroadcastResultHolder(initialCode, data, extras));
    for (final Wrapper wrapper : wrappers) {
      future = postIntent(wrapper, intent, future, abort, context);
    }
    final ListenableFuture<?> finalFuture = future;
    future.addListener(
        new Runnable() {
          @Override
          public void run() {
            getMainHandler(context)
                .post(
                    new Runnable() {
                      @Override
                      public void run() {
                        try {
                          finalFuture.get();
                        } catch (InterruptedException | ExecutionException e) {
                          throw new RuntimeException(e);
                        }
                      }
                    });
          }
        },
        directExecutor());
  }

  /**
   * Enforces that BroadcastReceivers invoked during an ordered broadcast run serially, passing
   * along their results.
   */
  private ListenableFuture<BroadcastResultHolder> postIntent(
      final Wrapper wrapper,
      final Intent intent,
      ListenableFuture<BroadcastResultHolder> oldResult,
      final AtomicBoolean abort,
      final Context context) {
    final Handler scheduler =
        (wrapper.scheduler != null) ? wrapper.scheduler : getMainHandler(context);
    return Futures.transformAsync(
        oldResult,
        new AsyncFunction<BroadcastResultHolder, BroadcastResultHolder>() {
          @Override
          public ListenableFuture<BroadcastResultHolder> apply(
              BroadcastResultHolder broadcastResultHolder) throws Exception {
            final BroadcastReceiver.PendingResult result =
                ShadowBroadcastPendingResult.create(
                    broadcastResultHolder.resultCode,
                    broadcastResultHolder.resultData,
                    broadcastResultHolder.resultExtras,
                    true /*ordered */);
            wrapper.broadcastReceiver.setPendingResult(result);
            scheduler.post(
                () -> {
                  ShadowBroadcastReceiver shadowBroadcastReceiver =
                      Shadow.extract(wrapper.broadcastReceiver);
                  shadowBroadcastReceiver.onReceive(context, intent, abort);
                });
            return BroadcastResultHolder.transform(result);
          }
        },
        directExecutor());
  }

  /**
   * Broadcasts the {@code Intent} by iterating through the registered receivers, invoking their
   * filters including permissions, and calling {@code onReceive(Application, Intent)} as
   * appropriate. Does not enqueue the {@code Intent} for later inspection.
   *
   * @param context
   * @param intent the {@code Intent} to broadcast todo: enqueue the Intent for later inspection
   */
  void sendBroadcastWithPermission(Intent intent, String receiverPermission, Context context) {
    sendBroadcastWithPermission(intent, receiverPermission, context, 0);
  }

  void sendBroadcastWithPermission(
      Intent intent, String receiverPermission, Context context, int resultCode) {
    List<Wrapper> wrappers = getAppropriateWrappers(intent, receiverPermission);
    postToWrappers(wrappers, intent, context, resultCode);
  }

  void sendOrderedBroadcastWithPermission(
      Intent intent, String receiverPermission, Context context) {
    List<Wrapper> wrappers = getAppropriateWrappers(intent, receiverPermission);
    // sort by the decrease of priorities
    sortByPriority(wrappers);

    postOrderedToWrappers(wrappers, intent, 0, null, null, context);
  }

  private void sortByPriority(List<Wrapper> wrappers) {
    Collections.sort(
        wrappers,
        new Comparator<Wrapper>() {
          @Override
          public int compare(Wrapper o1, Wrapper o2) {
            return Integer.compare(
                o2.getIntentFilter().getPriority(), o1.getIntentFilter().getPriority());
          }
        });
  }

  List<Intent> getBroadcastIntents() {
    return broadcastIntents;
  }

  Intent getNextStartedActivity() {
    if (startedActivities.isEmpty()) {
      return null;
    } else {
      return startedActivities.remove(startedActivities.size() - 1);
    }
  }

  Intent peekNextStartedActivity() {
    if (startedActivities.isEmpty()) {
      return null;
    } else {
      return startedActivities.get(startedActivities.size() - 1);
    }
  }

  /**
   * Clears all {@code Intent}s started by {@link #execStartActivity(Context, IBinder, IBinder,
   * Activity, Intent, int, Bundle)}, {@link #execStartActivity(Context, IBinder, IBinder, Fragment,
   * Intent, int, Bundle)}, and {@link #execStartActivity(Context, IBinder, IBinder, String, Intent,
   * int, Bundle)}.
   */
  void clearNextStartedActivities() {
    startedActivities.clear();
  }

  IntentForResult getNextStartedActivityForResult() {
    if (startedActivitiesForResults.isEmpty()) {
      return null;
    } else {
      return startedActivitiesForResults.remove(startedActivitiesForResults.size() - 1);
    }
  }

  IntentForResult peekNextStartedActivityForResult() {
    if (startedActivitiesForResults.isEmpty()) {
      return null;
    } else {
      return startedActivitiesForResults.get(startedActivitiesForResults.size() - 1);
    }
  }

  void checkActivities(boolean checkActivities) {
    this.checkActivities = checkActivities;
  }

  int getRequestCodeForIntent(Intent requestIntent) {
    Integer requestCode = intentRequestCodeMap.get(new Intent.FilterComparison(requestIntent));
    if (requestCode == null) {
      throw new RuntimeException(
          "No intent matches " + requestIntent + " among " + intentRequestCodeMap.keySet());
    }
    return requestCode;
  }

  protected ComponentName startService(Intent intent) {
    startedServices.add(new Intent.FilterComparison(intent));
    if (intent.getComponent() != null) {
      return intent.getComponent();
    }
    return new ComponentName("some.service.package", "SomeServiceName-FIXME");
  }

  boolean stopService(Intent name) {
    stoppedServices.add(new Intent.FilterComparison(name));
    return startedServices.contains(new Intent.FilterComparison(name));
  }

  void setComponentNameAndServiceForBindService(ComponentName name, IBinder service) {
    defaultServiceConnectionData = new ServiceConnectionDataWrapper(name, service);
  }

  void setComponentNameAndServiceForBindServiceForIntent(
      Intent intent, ComponentName name, IBinder service) {
    serviceConnectionDataForIntent.put(
        new Intent.FilterComparison(intent), new ServiceConnectionDataWrapper(name, service));
  }

  protected boolean bindService(
      final Intent intent, final ServiceConnection serviceConnection, int i) {
    boundServiceConnections.add(serviceConnection);
    unboundServiceConnections.remove(serviceConnection);
    if (unbindableActions.contains(intent.getAction())) {
      return false;
    }
    startedServices.add(new Intent.FilterComparison(intent));
    Handler handler = new Handler(Looper.getMainLooper());
    handler.post(
        () -> {
          final ServiceConnectionDataWrapper serviceConnectionDataWrapper;
          final Intent.FilterComparison filterComparison = new Intent.FilterComparison(intent);
          if (serviceConnectionDataForIntent.containsKey(filterComparison)) {
            serviceConnectionDataWrapper = serviceConnectionDataForIntent.get(filterComparison);
          } else {
            serviceConnectionDataWrapper = defaultServiceConnectionData;
          }
          serviceConnectionDataForServiceConnection.put(
              serviceConnection, serviceConnectionDataWrapper);
          serviceConnection.onServiceConnected(
              serviceConnectionDataWrapper.componentNameForBindService,
              serviceConnectionDataWrapper.binderForBindService);
        });
    return true;
  }

  protected void unbindService(final ServiceConnection serviceConnection) {
    if (unbindServiceShouldThrowIllegalArgument) {
      throw new IllegalArgumentException();
    }

    unboundServiceConnections.add(serviceConnection);
    boundServiceConnections.remove(serviceConnection);
    Handler handler = new Handler(Looper.getMainLooper());
    handler.post(
        () -> {
          final ServiceConnectionDataWrapper serviceConnectionDataWrapper;
          if (serviceConnectionDataForServiceConnection.containsKey(serviceConnection)) {
            serviceConnectionDataWrapper =
                serviceConnectionDataForServiceConnection.get(serviceConnection);
          } else {
            serviceConnectionDataWrapper = defaultServiceConnectionData;
          }
          serviceConnection.onServiceDisconnected(
              serviceConnectionDataWrapper.componentNameForBindService);
        });
  }

  protected List<ServiceConnection> getBoundServiceConnections() {
    return boundServiceConnections;
  }

  void setUnbindServiceShouldThrowIllegalArgument(boolean flag) {
    unbindServiceShouldThrowIllegalArgument = flag;
  }

  protected List<ServiceConnection> getUnboundServiceConnections() {
    return unboundServiceConnections;
  }

  void declareActionUnbindable(String action) {
    unbindableActions.add(action);
  }

  public List<String> getUnbindableActions() {
    return unbindableActions;
  }

  /**
   * Consumes the most recent {@code Intent} started by {@link
   * #startService(android.content.Intent)} and returns it.
   *
   * @return the most recently started {@code Intent}
   */
  Intent getNextStartedService() {
    if (startedServices.isEmpty()) {
      return null;
    } else {
      return startedServices.remove(0).getIntent();
    }
  }

  /**
   * Returns the most recent {@code Intent} started by {@link #startService(android.content.Intent)}
   * without consuming it.
   *
   * @return the most recently started {@code Intent}
   */
  Intent peekNextStartedService() {
    if (startedServices.isEmpty()) {
      return null;
    } else {
      return startedServices.get(0).getIntent();
    }
  }

  /** Clears all {@code Intent} started by {@link #startService(android.content.Intent)}. */
  void clearStartedServices() {
    startedServices.clear();
  }

  /**
   * Consumes the {@code Intent} requested to stop a service by {@link
   * #stopService(android.content.Intent)} from the bottom of the stack of stop requests.
   */
  Intent getNextStoppedService() {
    if (stoppedServices.isEmpty()) {
      return null;
    } else {
      return stoppedServices.remove(0).getIntent();
    }
  }

  void sendStickyBroadcast(Intent intent, Context context) {
    stickyIntents.put(intent.getAction(), intent);
    sendBroadcast(intent, context);
  }

  void sendBroadcast(Intent intent, Context context) {
    sendBroadcastWithPermission(intent, null, context);
  }

  Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, Context context) {
    return registerReceiver(receiver, filter, null, null, context);
  }

  Intent registerReceiver(
      BroadcastReceiver receiver,
      IntentFilter filter,
      String broadcastPermission,
      Handler scheduler,
      Context context) {
    return registerReceiverWithContext(receiver, filter, broadcastPermission, scheduler, context);
  }

  Intent registerReceiverWithContext(
      BroadcastReceiver receiver,
      IntentFilter filter,
      String broadcastPermission,
      Handler scheduler,
      Context context) {
    if (receiver != null) {
      registeredReceivers.add(
          new Wrapper(receiver, filter, context, broadcastPermission, scheduler));
    }
    return processStickyIntents(filter, receiver, context);
  }

  private Intent processStickyIntents(
      IntentFilter filter, BroadcastReceiver receiver, Context context) {
    Intent result = null;
    for (Intent stickyIntent : stickyIntents.values()) {
      if (filter.matchAction(stickyIntent.getAction())) {
        if (result == null) {
          result = stickyIntent;
        }
        if (receiver != null) {
          receiver.setPendingResult(ShadowBroadcastPendingResult.createSticky(stickyIntent));
          receiver.onReceive(context, stickyIntent);
          receiver.setPendingResult(null);
        } else if (result != null) {
          break;
        }
      }
    }
    return result;
  }

  void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
    boolean found = false;
    Iterator<Wrapper> iterator = registeredReceivers.iterator();
    while (iterator.hasNext()) {
      Wrapper wrapper = iterator.next();
      if (wrapper.broadcastReceiver == broadcastReceiver) {
        iterator.remove();
        found = true;
      }
    }
    if (!found) {
      throw new IllegalArgumentException("Receiver not registered: " + broadcastReceiver);
    }
  }

  void clearRegisteredReceivers() {
    registeredReceivers.clear();
  }

  /** @deprecated use PackageManager.queryBroadcastReceivers instead */
  @Deprecated
  boolean hasReceiverForIntent(Intent intent) {
    for (Wrapper wrapper : registeredReceivers) {
      if (wrapper.intentFilter.matchAction(intent.getAction())) {
        return true;
      }
    }
    return false;
  }

  /** @deprecated use PackageManager.queryBroadcastReceivers instead */
  @Deprecated
  List<BroadcastReceiver> getReceiversForIntent(Intent intent) {
    ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
    for (Wrapper wrapper : registeredReceivers) {
      if (wrapper.intentFilter.matchAction(intent.getAction())) {
        broadcastReceivers.add(wrapper.getBroadcastReceiver());
      }
    }
    return broadcastReceivers;
  }

  /** @return list of {@link Wrapper}s for registered receivers */
  List<Wrapper> getRegisteredReceivers() {
    return registeredReceivers;
  }

  int checkPermission(String permission, int pid, int uid) {
    Set<String> grantedPermissionsForPidUid = grantedPermissionsMap.get(new Pair(pid, uid));
    return grantedPermissionsForPidUid != null && grantedPermissionsForPidUid.contains(permission)
        ? PERMISSION_GRANTED
        : PERMISSION_DENIED;
  }

  void grantPermissions(String... permissionNames) {
    grantPermissions(Process.myPid(), Process.myUid(), permissionNames);
  }

  void grantPermissions(int pid, int uid, String... permissions) {
    Set<String> grantedPermissionsForPidUid = grantedPermissionsMap.get(new Pair<>(pid, uid));
    if (grantedPermissionsForPidUid == null) {
      grantedPermissionsForPidUid = new HashSet<>();
      grantedPermissionsMap.put(new Pair<>(pid, uid), grantedPermissionsForPidUid);
    }
    Collections.addAll(grantedPermissionsForPidUid, permissions);
  }

  void denyPermissions(String... permissionNames) {
    denyPermissions(Process.myPid(), Process.myUid(), permissionNames);
  }

  void denyPermissions(int pid, int uid, String... permissions) {
    Set<String> grantedPermissionsForPidUid = grantedPermissionsMap.get(new Pair<>(pid, uid));
    if (grantedPermissionsForPidUid != null) {
      for (String permissionName : permissions) {
        grantedPermissionsForPidUid.remove(permissionName);
      }
    }
  }

  private boolean hasMatchingPermission(String permission1, String permission2) {
    return permission1 == null ? permission2 == null : permission1.equals(permission2);
  }

  private Handler getMainHandler(Context context) {
    if (mainHandler == null) {
      mainHandler = new Handler(context.getMainLooper());
    }
    return mainHandler;
  }

  /** Accessor interface for {@link Instrumentation}'s internals. */
  @ForType(Instrumentation.class)
  public interface _Instrumentation_ {
    // <= JELLY_BEAN_MR1:
    void init(
        ActivityThread thread,
        Context instrContext,
        Context appContext,
        ComponentName component,
        @WithType("android.app.IInstrumentationWatcher") Object watcher);

    // > JELLY_BEAN_MR1:
    void init(
        ActivityThread thread,
        Context instrContext,
        Context appContext,
        ComponentName component,
        @WithType("android.app.IInstrumentationWatcher") Object watcher,
        @WithType("android.app.IUiAutomationConnection") Object uiAutomationConnection);
  }

  private static final class BroadcastResultHolder {
    private final int resultCode;
    private final String resultData;
    private final Bundle resultExtras;

    private BroadcastResultHolder(int resultCode, String resultData, Bundle resultExtras) {
      this.resultCode = resultCode;
      this.resultData = resultData;
      this.resultExtras = resultExtras;
    }

    private static ListenableFuture<BroadcastResultHolder> transform(
        BroadcastReceiver.PendingResult result) {
      ShadowBroadcastPendingResult shadowBroadcastPendingResult = Shadow.extract(result);
      return Futures.transform(
          shadowBroadcastPendingResult.getFuture(),
          pendingResult ->
              new BroadcastResultHolder(
                  pendingResult.getResultCode(),
                  pendingResult.getResultData(),
                  pendingResult.getResultExtras(false)),
          directExecutor());
    }
  }

  private static class ServiceConnectionDataWrapper {
    public final ComponentName componentNameForBindService;
    public final IBinder binderForBindService;

    private ServiceConnectionDataWrapper(
        ComponentName componentNameForBindService, IBinder binderForBindService) {
      this.componentNameForBindService = componentNameForBindService;
      this.binderForBindService = binderForBindService;
    }
  }

  public static Instrumentation getInstrumentation() {
    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    if (activityThread != null) {
      return activityThread.getInstrumentation();
    }
    return null;
  }
}


package org.robolectric.shadows;

import android.graphics.Color;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Implements(Color.class)
public class ShadowColor {
  /**
   * This is implemented in native code in the Android SDK.
   *
   * <p>Since HSV == HSB then the implementation from {@link java.awt.Color} can be used, with a
   * small adjustment to the representation of the hue.
   *
   * <p>{@link java.awt.Color} represents hue as 0..1 (where 1 == 100% == 360 degrees), while {@link
   * android.graphics.Color} represents hue as 0..360 degrees. The correct hue can be calculated by
   * multiplying with 360.
   *
   * @param red Red component
   * @param green Green component
   * @param blue Blue component
   * @param hsv Array to store HSV components
   */
  @Implementation
  protected static void RGBToHSV(int red, int green, int blue, float hsv[]) {
    java.awt.Color.RGBtoHSB(red, green, blue, hsv);
    hsv[0] = hsv[0] * 360;
  }

  @Implementation
  protected static int HSVToColor(int alpha, float hsv[]) {
    int rgb = java.awt.Color.HSBtoRGB(hsv[0] / 360, hsv[1], hsv[2]);
    return Color.argb(alpha, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
  }
}


package org.robolectric.shadows;

import android.opengl.Matrix;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Implements(Matrix.class)
public class ShadowOpenGLMatrix {

  /**
   * Multiplies two 4x4 matrices together and stores the result in a third 4x4 matrix. In matrix
   * notation: result = lhs x rhs. Due to the way matrix multiplication works, the result matrix
   * will have the same effect as first multiplying by the rhs matrix, then multiplying by the lhs
   * matrix. This is the opposite of what you might expect.
   *
   *
The same float array may be passed for result, lhs, and/or rhs. However, the result element
   * values are undefined if the result elements overlap either the lhs or rhs elements.
   *
   * @param result The float array that holds the result.
   * @param resultOffset The offset into the result array where the result is stored.
   * @param lhs The float array that holds the left-hand-side matrix.
   * @param lhsOffset The offset into the lhs array where the lhs is stored
   * @param rhs The float array that holds the right-hand-side matrix.
   * @param rhsOffset The offset into the rhs array where the rhs is stored.
   * @throws IllegalArgumentException if result, lhs, or rhs are null, or if resultOffset + 16 >
   *     result.length or lhsOffset + 16 > lhs.length or rhsOffset + 16 > rhs.length.
   */
  @Implementation
  protected static void multiplyMM(
      float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset) {
    if (result == null) {
      throw new IllegalArgumentException("result == null");
    }
    if (lhs == null) {
      throw new IllegalArgumentException("lhs == null");
    }
    if (rhs == null) {
      throw new IllegalArgumentException("rhs == null");
    }
    if (resultOffset + 16 > result.length) {
      throw new IllegalArgumentException("resultOffset + 16 > result.length");
    }
    if (lhsOffset + 16 > lhs.length) {
      throw new IllegalArgumentException("lhsOffset + 16 > lhs.length");
    }
    if (rhsOffset + 16 > rhs.length) {
      throw new IllegalArgumentException("rhsOffset + 16 > rhs.length");
    }
    for (int i = 0; i < 4; i++) {
      final float rhs_i0 = rhs[I(i, 0, rhsOffset)];
      float ri0 = lhs[I(0, 0, lhsOffset)] * rhs_i0;
      float ri1 = lhs[I(0, 1, lhsOffset)] * rhs_i0;
      float ri2 = lhs[I(0, 2, lhsOffset)] * rhs_i0;
      float ri3 = lhs[I(0, 3, lhsOffset)] * rhs_i0;
      for (int j = 1; j < 4; j++) {
        final float rhs_ij = rhs[I(i, j, rhsOffset)];
        ri0 += lhs[I(j, 0, lhsOffset)] * rhs_ij;
        ri1 += lhs[I(j, 1, lhsOffset)] * rhs_ij;
        ri2 += lhs[I(j, 2, lhsOffset)] * rhs_ij;
        ri3 += lhs[I(j, 3, lhsOffset)] * rhs_ij;
      }
      result[I(i, 0, resultOffset)] = ri0;
      result[I(i, 1, resultOffset)] = ri1;
      result[I(i, 2, resultOffset)] = ri2;
      result[I(i, 3, resultOffset)] = ri3;
    }
  }

  /**
   * Multiplies a 4 element vector by a 4x4 matrix and stores the result in a 4-element column
   * vector. In matrix notation: result = lhs x rhs
   *
   *
The same float array may be passed for resultVec, lhsMat, and/or rhsVec. However, the
   * resultVec element values are undefined if the resultVec elements overlap either the lhsMat or
   * rhsVec elements.
   *
   * @param resultVec The float array that holds the result vector.
   * @param resultVecOffset The offset into the result array where the result vector is stored.
   * @param lhsMat The float array that holds the left-hand-side matrix.
   * @param lhsMatOffset The offset into the lhs array where the lhs is stored
   * @param rhsVec The float array that holds the right-hand-side vector.
   * @param rhsVecOffset The offset into the rhs vector where the rhs vector is stored.
   * @throws IllegalArgumentException if resultVec, lhsMat, or rhsVec are null, or if
   *     resultVecOffset + 4 > resultVec.length or lhsMatOffset + 16 > lhsMat.length or rhsVecOffset
   *     + 4 > rhsVec.length.
   */
  @Implementation
  protected static void multiplyMV(
      float[] resultVec,
      int resultVecOffset,
      float[] lhsMat,
      int lhsMatOffset,
      float[] rhsVec,
      int rhsVecOffset) {
    if (resultVec == null) {
      throw new IllegalArgumentException("resultVec == null");
    }
    if (lhsMat == null) {
      throw new IllegalArgumentException("lhsMat == null");
    }
    if (rhsVec == null) {
      throw new IllegalArgumentException("rhsVec == null");
    }
    if (resultVecOffset + 4 > resultVec.length) {
      throw new IllegalArgumentException("resultVecOffset + 4 > resultVec.length");
    }
    if (lhsMatOffset + 16 > lhsMat.length) {
      throw new IllegalArgumentException("lhsMatOffset + 16 > lhsMat.length");
    }
    if (rhsVecOffset + 4 > rhsVec.length) {
      throw new IllegalArgumentException("rhsVecOffset + 4 > rhsVec.length");
    }
    final float x = rhsVec[rhsVecOffset + 0];
    final float y = rhsVec[rhsVecOffset + 1];
    final float z = rhsVec[rhsVecOffset + 2];
    final float w = rhsVec[rhsVecOffset + 3];
    resultVec[resultVecOffset + 0] = lhsMat[I(0, 0, lhsMatOffset)] * x + lhsMat[I(1, 0, lhsMatOffset)] * y + lhsMat[I(2, 0, lhsMatOffset)] * z + lhsMat[I(3, 0, lhsMatOffset)] * w;
    resultVec[resultVecOffset + 1] = lhsMat[I(0, 1, lhsMatOffset)] * x + lhsMat[I(1, 1, lhsMatOffset)] * y + lhsMat[I(2, 1, lhsMatOffset)] * z + lhsMat[I(3, 1, lhsMatOffset)] * w;
    resultVec[resultVecOffset + 2] = lhsMat[I(0, 2, lhsMatOffset)] * x + lhsMat[I(1, 2, lhsMatOffset)] * y + lhsMat[I(2, 2, lhsMatOffset)] * z + lhsMat[I(3, 2, lhsMatOffset)] * w;
    resultVec[resultVecOffset + 3] = lhsMat[I(0, 3, lhsMatOffset)] * x + lhsMat[I(1, 3, lhsMatOffset)] * y + lhsMat[I(2, 3, lhsMatOffset)] * z + lhsMat[I(3, 3, lhsMatOffset)] * w;
  }

  private static int I(int i, int j, int offset) {
    // #define I(_i, _j) ((_j)+ 4*(_i))
    return offset + j + 4 * i;
  }

}


package org.robolectric.shadows;

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

import android.os.Trace;
import android.util.Log;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
import javax.annotation.concurrent.GuardedBy;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/**
 * Shadow implementation for {@link Trace}, which stores the traces locally in arrays (unlike the
 * real implementation) and allows reading them.
 *
 *
The shadow doesn't enforce the constrains by default (e.g., null section names, or incorrect
 * {@link ShadowTrace.beginSection(String)} / {@link ShadowTrace.endSection()} sequences), but can
 * be configured to do so by calling {@link ShadowTrace.setCrashOnIncorrectUsage(boolean)}.
 */
@Implements(Trace.class)
public class ShadowTrace {
  private static final String TAG = "ShadowTrace";

  @GuardedBy("lock")
  private static final Deque<String> currentSections = new ArrayDeque<>();

  @GuardedBy("lock")
  private static final Queue<String> previousSections = new ArrayDeque<>();

  private static final boolean CRASH_ON_INCORRECT_USAGE_DEFAULT = true;
  private static boolean crashOnIncorrectUsage = CRASH_ON_INCORRECT_USAGE_DEFAULT;
  private static boolean appTracingAllowed = true;
  private static final Object lock = new Object();

  private static final long TRACE_TAG_APP = 1L << 12;
  private static final int MAX_SECTION_NAME_LEN = 127;

  /** Starts a new trace section with given name. */
  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected static void beginSection(String sectionName) {
    if (Trace.isTagEnabled(TRACE_TAG_APP)) {
      if (crashOnIncorrectUsage) {
        if (sectionName.length() > MAX_SECTION_NAME_LEN) {
          throw new IllegalArgumentException("sectionName is too long");
        }
      } else if (sectionName == null) {
        Log.w(TAG, "Section name cannot be null");
        return;
      } else if (sectionName.length() > MAX_SECTION_NAME_LEN) {
        Log.w(TAG, "Section name is too long");
        return;
      }

      synchronized (lock) {
        currentSections.addFirst(sectionName);
      }
    }
  }

  /**
   * Ends the most recent active trace section.
   *
   * @throws {@link AssertionError} if called without any active trace section.
   */
  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected static void endSection() {
    if (Trace.isTagEnabled(TRACE_TAG_APP)) {
      synchronized (lock) {
        if (currentSections.isEmpty()) {
          Log.e(TAG, "Trying to end a trace section that was never started");
          return;
        }

        previousSections.offer(currentSections.removeFirst());
      }
    }
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected static boolean isTagEnabled(long traceTag) {
    if (traceTag == TRACE_TAG_APP) {
      return appTracingAllowed;
    }

    return directlyOn(Trace.class, "isTagEnabled", ClassParameter.from(long.class, traceTag));
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected static void setAppTracingAllowed(boolean appTracingAllowed) {
    ShadowTrace.appTracingAllowed = appTracingAllowed;
  }

  /** Returns a stack of the currently active trace sections. */
  public static Deque<String> getCurrentSections() {
    synchronized (lock) {
      return new ArrayDeque<>(currentSections);
    }
  }

  /** Returns a queue of all the previously active trace sections. */
  public static Queue<String> getPreviousSections() {
    synchronized (lock) {
      return new ArrayDeque<>(previousSections);
    }
  }

  /**
   * Do not use this method unless absolutely necessary. Prefer fixing the tests instead.
   *
   *
Sets whether to crash on incorrect usage (e.g., calling {@link #endSection()} before {@link
   * beginSection(String)}. Default value - {@code false}.
   */
  public static void doNotUseSetCrashOnIncorrectUsage(boolean crashOnIncorrectUsage) {
    ShadowTrace.crashOnIncorrectUsage = crashOnIncorrectUsage;
  }

  /** Resets internal lists of active trace sections. */
  @Resetter
  public static void reset() {
    synchronized (lock) {
      currentSections.clear();
      previousSections.clear();
    }
    crashOnIncorrectUsage = CRASH_ON_INCORRECT_USAGE_DEFAULT;
  }
}


package org.robolectric.util.inject;

import com.google.common.annotations.VisibleForTesting;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import org.robolectric.util.Util;

/**
 * A tiny dependency injection and plugin helper for Robolectric.
 *
 * Dependencies may be retrieved explicitly by calling {@link #getInstance}; transitive dependencies
 * will be automatically injected as needed. For a given injector, all calls to
 * {@link #getInstance} are idempotent.
 *
 * Dependencies are identified by an interface or class, and optionally by a name specified with
 * `@`{@link Named}.
 *
 * ### Dependency Resolution
 * When a dependency is requested, an implementation is sought.
 *
 * The injector looks for any instance that has been previously found for the given interface, or
 * that has been explicitly registered with {@link Builder#bind(Class, Object)} or
 * {@link Builder#bind(Key, Object)}. If none is found, the injector searches for an implementing
 * class from the following sources, in order:
 *
 * 1. Explicitly-registered implementations registered with {@link Builder#bind(Class, Class)}.
 * 1. If the dependency type is an array or {@link Collection}, then its component type
 *    is recursively sought using {@link PluginFinder#findPlugins(Class)} and an array or collection
 *    of those instances is returned.
 * 1. Plugin implementations published as {@link java.util.ServiceLoader} services under the
 *    dependency type (see also {@link PluginFinder#findPlugin(Class)}).
 * 1. Fallback default implementation classes registered with
 *    {@link Builder#bindDefault(Class, Class)}.
 * 1. If the dependency type is a concrete class, then the dependency type itself.
 *
 * If the injector has a superinjector, it is always consulted first (with the exception of
 * interfaces annotated `@`{@link AutoFactory}; see Scopes below).
 *
 * If no implementing class is found in the injector or any superinjector, an exception is thrown.
 *
 * ### Injection
 * When the injector has determined an implementing class, it attempts to instantiate it. It
 * searches for a constructor in the following order:
 *
 * 1. A singular public constructor annotated `@`{@link Inject}. (If multiple constructors are
 *    `@Inject` annotated, the injector will throw an exception.)
 * 1. A singular public constructor of any arity.
 * 1. If no constructor has yet been found, the injector will throw an exception.
 *
 * Any constructor parameters are treated as further dependencies, and the injector will recursively
 * attempt to resolve an implementation for each before invoking the constructor and thereby
 * instantiating the original dependency implementation.
 *
 * ### Scopes
 *  If the dependency type is an interface annotated `@`{@link AutoFactory}, then a factory object
 *  implementing that interface is created; a new scoped injector is created for every method
 *  call to the factory, with parameter arguments registered on the scoped injector.
 *
 * ### Thread Safety
 * All methods are MT-safe.
 */
@SuppressWarnings({"NewApi", "AndroidJdkLibsChecker"})
public class Injector {

  private static final Key<Object> INJECTOR_KEY = new Key<>(Injector.class);

  private final Injector superInjector;
  private final PluginFinder pluginFinder;

  @GuardedBy("this")
  private final Map<Key<?>, Provider<?>> providers;
  private final Map<Key<?>, Class<?>> defaultImpls;

  /** Creates a new empty injector. */
  public Injector() {
    this(new PluginFinder());
  }

  @VisibleForTesting
  Injector(PluginFinder pluginFinder) {
    this(null, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(),
        pluginFinder);
  }

  /** Creates a new injector based on values from a Builder. */
  private Injector(Injector superInjector, Map<Key<?>, Provider<?>> providers,
      Map<Key<?>, Class<?>> explicitImpls, Map<Key<?>, Class<?>> defaultImpls,
      PluginFinder pluginFinder) {
    this.superInjector = superInjector;

    this.providers = new HashMap<>(providers);
    for (Map.Entry<Key<?>, Class<?>> e : explicitImpls.entrySet()) {
      this.providers.put(e.getKey(), memoized(() -> inject(e.getValue())));
    }

    this.defaultImpls = new HashMap<>(defaultImpls);
    this.pluginFinder = pluginFinder;
  }

  /** Builder for {@link Injector}. */
  public static class Builder {
    private final Injector superInjector;
    private final Map<Key<?>, Provider<?>> providers = new HashMap<>();
    private final Map<Key<?>, Class<?>> explicitImpls = new HashMap<>();
    private final Map<Key<?>, Class<?>> defaultImpls = new HashMap<>();
    private final PluginFinder pluginFinder;

    /** Creates a new builder. */
    public Builder() {
      this(null, (ClassLoader) null);
    }

    /** Creates a new builder using the specified ClassLoader for plugin loading. */
    public Builder(ClassLoader classLoader) {
      this(null, classLoader);
    }

    /** Creates a new builder with a parent injector. */
    public Builder(Injector superInjector) {
      this(superInjector, (ClassLoader) null);
    }

    /**
     * Creates a new builder with a parent injector and the specified ClassLoader for plugin
     * loading.
     */
    public Builder(Injector superInjector, ClassLoader classLoader) {
      this(superInjector, new PluginFinder(classLoader));
    }

    @VisibleForTesting
    Builder(Injector superInjector, PluginFinder pluginFinder) {
      this.superInjector = superInjector;
      this.pluginFinder = pluginFinder;
    }

    /** Registers an instance for the given dependency type. */
    public <T> Builder bind(@Nonnull Class<T> type, @Nonnull T instance) {
      return bind(new Key<>(type), instance);
    }

    /** Registers an instance for the given key. */
    public <T> Builder bind(Key<T> key, @Nonnull T instance) {
      providers.put(key, () -> instance);
      return this;
    }

    /** Registers an implementing class for the given dependency type. */
    public <T> Builder bind(@Nonnull Class<T> type, @Nonnull Class<? extends T> implementingClass) {
      explicitImpls.put(new Key<>(type), implementingClass);
      return this;
    }

    /** Registers a fallback implementing class for the given dependency type. */
    public <T> Builder bindDefault(
        @Nonnull Class<T> type, @Nonnull Class<? extends T> defaultImplementingClass) {
      defaultImpls.put(new Key<>(type), defaultImplementingClass);
      return this;
    }

    /** Builds an injector as previously configured. */
    public Injector build() {
      return new Injector(superInjector, providers, explicitImpls, defaultImpls, pluginFinder);
    }
  }

  /** Finds an instance for the given class. Calls are guaranteed idempotent. */
  @Nonnull public <T> T getInstance(@Nonnull Class<T> type) {
    return getInstance(new Key<>(type));
  }

  /** Finds an instance for the given key. Calls are guaranteed idempotent. */
  @Nonnull private <T> T getInstance(@Nonnull Key<T> key) {
    try {
      return getInstanceInternal(key);
    } catch (UnsatisfiedDependencyException e) {
      throw e.asInjectionException(key);
    }
  }

  private <T> T getInstanceInternal(@Nonnull Key<T> key) {
    // If we have a superinjector, check it for a provider first.
    if (superInjector != null) {
      try {
        return superInjector.getInstanceInternal(key);
      } catch (UnsatisfiedDependencyException | InjectionException e) {
        // that's fine, we'll try locally next...
      }
    }

    return getProvider(key).get();
  }

  public Injector.Builder newScopeBuilder(ClassLoader classLoader) {
    return new Injector.Builder(this, classLoader);
  }

  @Nonnull private <T> Provider<T> memoized(@Nonnull Class<? extends T> implementingClass) {
    return memoized(() -> inject(implementingClass));
  }

  @Nonnull private <T> Provider<T> memoized(@Nonnull Provider<T> tProvider) {
    return new MemoizingProvider<>(tProvider);
  }

  @SuppressWarnings("unchecked")
  @Nonnull private <T> T inject(@Nonnull Class<? extends T> implementingClass) {
    Constructor<T> ctor;
    try {
      ctor = findConstructor(implementingClass);
    } catch (IllegalArgumentException e) {
      throw new InjectionException(implementingClass, e);
    }

    Object[] params = resolveDependencies(ctor);
    try {
      return ctor.newInstance(params);
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
      throw Util.sneakyThrow(e.getCause());
    }
  }

  private <T> Constructor<T> findConstructor(@Nonnull Class<? extends T> implementingClass) {
    List<Constructor<T>> injectCtors = new ArrayList<>();
    List<Constructor<T>> otherCtors = new ArrayList<>();

    for (Constructor<?> ctor : implementingClass.getConstructors()) {
      if (ctor.isAnnotationPresent(Inject.class)) {
        injectCtors.add((Constructor<T>) ctor);
      } else {
        otherCtors.add((Constructor<T>) ctor);
      }
    }

    if (injectCtors.size() > 1) { // ambiguous @Inject constructors
      throw new InjectionException(implementingClass, "multiple public @Inject constructors");
    } else if (injectCtors.size() == 1) { // single @Inject constructor, bingo!
      return injectCtors.get(0);
    } else if (otherCtors.size() > 1) { // ambiguous non-@Inject constructors
      throw new InjectionException(implementingClass, "multiple public constructors");
    } else if (otherCtors.size() == 1) { // single public constructor, bingo!
      return otherCtors.get(0);
    } else {
      throw new InjectionException(implementingClass, "no public constructor");
    }
  }

  private Object[] resolveDependencies(Executable ctor) {
    final Object[] params = new Object[ctor.getParameterCount()];

    AnnotatedType[] paramTypes = ctor.getAnnotatedParameterTypes();
    Annotation[][] parameterAnnotations = ctor.getParameterAnnotations();
    for (int i = 0; i < paramTypes.length; i++) {
      AnnotatedType paramType = paramTypes[i];
      String name = findName(parameterAnnotations[i]);
      Key<?> key = new Key<>(paramType.getType(), name);
      if (key.equals(INJECTOR_KEY)) {
        params[i] = this;
      } else {
        try {
          params[i] = getInstanceInternal(key);
        } catch (UnsatisfiedDependencyException e) {
          throw new UnsatisfiedDependencyException(new Key<>(ctor.getDeclaringClass()), e);
        }
      }
    }
    return params;
  }

  private String findName(Annotation[] annotations) {
    for (Annotation annotation : annotations) {
      if (annotation instanceof Named) {
        return ((Named) annotation).value();
      }
    }
    return null;
  }

  /**
   * Finds a provider for the given key.
   *
   * Calls are guaranteed idempotent and non-blocking.
   */
  @SuppressWarnings("unchecked")
  @Nonnull
  private synchronized <T> Provider<T> getProvider(final Key<T> key) {
    // Previously-gotten providers (including those from subinjectors) will already be present.
    return (Provider<T>) providers.computeIfAbsent(key, k -> {
      // @AutoFactory requests are always handled by the top-level injector.
      if (key.isAutoFactory()) {
        return memoized(new ScopeBuilderProvider<>(key.getDependencyClass()));
      }

      // Find a provider locally.
      return findLocalProvider(key);
    });
  }

  private <T> Provider<T> findLocalProvider(Key<T> key) throws UnsatisfiedDependencyException {
    // If it's an array or collection, look for plugins.
    if (key.isArray()) {
      Provider<T> tProvider = new ArrayProvider(key.getComponentType());
      return memoized(tProvider);
    } else if (key.isCollection()) {
      Provider<T> tProvider = new ListProvider(key.getComponentType());
      return memoized(tProvider);
    }

    // Attempt to resolve an implementation class.
    Class<T> dependencyClass = key.getDependencyClass();

    // Try to find a solitary plugin...
    Class<? extends T> implClass = pluginFinder.findPlugin(dependencyClass);

    // ... or a default implementation class, if configured...
    if (implClass == null) {
      implClass = getDefaultImpl(key);
    }

    // ... otherwise if the dependency class is concrete (and not primitive or java.*), just use it.
    if (implClass == null && isConcrete(dependencyClass) && !isSystem(dependencyClass)) {
      implClass = dependencyClass;
    }

    if (implClass != null) {
      // Found an implementation class!
      return memoized(implClass);
    }

    // No luck.
    throw new UnsatisfiedDependencyException(key, null);
  }

  private <T> boolean isConcrete(Class<T> clazz) {
    return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers());
  }

  private <T> Class<? extends T> getDefaultImpl(Key<T> key) {
    Class<?> aClass = defaultImpls.get(key);
    return (Class<? extends T>) aClass;
  }

  private boolean isSystem(Class<?> clazz) {
    if (clazz.isPrimitive()) {
      return true;
    }
    Package aPackage = clazz.getPackage();
    return aPackage == null || aPackage.getName().startsWith("java.");
  }

  /** Identifies an injection point. */
  public static class Key<T> {

    @Nonnull
    private final Type theInterface;
    private final String name;

    private Key(@Nonnull Type theInterface) {
      this(theInterface, null);
    }

    public Key(Type theInterface, String name) {
      this.theInterface = theInterface;
      this.name = name;
    }

    Class<T> getDependencyClass() {
      //noinspection unchecked
      return (Class<T>) theInterface;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof Key)) {
        return false;
      }
      Key key = (Key) o;
      return theInterface.equals(key.theInterface) && Objects.equals(name, key.name);
    }

    @Override
    public int hashCode() {
      return Objects.hash(theInterface, name);
    }

    @Override
    public String toString() {
      StringBuilder buf = new StringBuilder();
      buf.append("Key<").append(theInterface);
      if (name != null) {
        buf.append(" named \"")
            .append(name)
            .append("\"");
      }
      buf.append(">");
      return buf.toString();
    }

    String toShortString() {
      StringBuilder buf = new StringBuilder();
      buf.append(theInterface instanceof Class
          ? ((Class) theInterface).getSimpleName()
          : theInterface.getTypeName());
      if (name != null) {
        buf.append(" \"")
            .append(name)
            .append("\"");
      }
      return buf.toString();
    }

    public boolean isArray() {
      return (theInterface instanceof Class && ((Class) theInterface).isArray())
          || theInterface instanceof GenericArrayType;
    }

    public boolean isCollection() {
      if (theInterface instanceof ParameterizedType) {
        Type rawType = ((ParameterizedType) theInterface).getRawType();
        return Collection.class.isAssignableFrom((Class<?>) rawType);
      }
      return false;
    }

    Class<?> getComponentType() {
      if (isArray()) {
        if (theInterface instanceof Class) {
          return ((Class) theInterface).getComponentType();
        } else if (theInterface instanceof GenericArrayType) {
          Type genericComponentType = ((GenericArrayType) theInterface).getGenericComponentType();
          return (Class<?>) ((ParameterizedType) genericComponentType).getRawType();
        } else {
          throw new InjectionException(this, new IllegalArgumentException());
        }
      } else if (isCollection() && theInterface instanceof ParameterizedType) {
        return (Class) ((ParameterizedType) theInterface).getActualTypeArguments()[0];
      } else {
        throw new IllegalStateException(theInterface + "...?");
      }
    }

    boolean isAutoFactory() {
      return theInterface instanceof Class
          && ((Class) theInterface).isAnnotationPresent(AutoFactory.class);
    }
  }

  private static class MemoizingProvider<T> implements Provider<T> {

    private Provider<T> delegate;
    private T instance;

    private MemoizingProvider(Provider<T> delegate) {
      this.delegate = delegate;
    }

    @Override
    public synchronized T get() {
      if (instance == null) {
        instance = delegate.get();
        delegate = null;
      }
      return instance;
    }
  }

  private class ListProvider<T> implements Provider<List<T>> {
    private final Class<T> clazz;

    ListProvider(Class<T> clazz) {
      this.clazz = clazz;
    }

    @Override
    public List<T> get() {
      List<T> plugins = new ArrayList<>();
      for (Class<? extends T> pluginClass : pluginFinder.findPlugins(clazz)) {
        plugins.add(inject(pluginClass));
      }
      return Collections.unmodifiableList(plugins);
    }
  }

  private class ArrayProvider<T> implements Provider<T[]> {
    private final ListProvider<T> listProvider;

    ArrayProvider(Class<T> clazz) {
      this.listProvider = new ListProvider<>(clazz);
    }

    @Override
    public T[] get() {
      T[] emptyArray = (T[]) Array.newInstance(listProvider.clazz, 0);
      return listProvider.get().toArray(emptyArray);
    }
  }

  private class ScopeBuilderProvider<T> implements Provider<T> {

    private final Class<T> clazz;

    public ScopeBuilderProvider(Class<T> clazz) {
      this.clazz = clazz;
    }

    @Override
    public T get() {
      return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
          (proxy, method, args) -> create(method, args));
    }

    private Object create(Method method, Object[] args) {
      Builder subBuilder = new Injector.Builder(Injector.this, pluginFinder);
      if (method.getParameterCount() > 0) {
        AnnotatedType[] parameterTypes = method.getAnnotatedParameterTypes();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < args.length; i++) {
          Type paramType = parameterTypes[i].getType();
          String name = findName(parameterAnnotations[i]);
          Object arg = args[i];
          subBuilder.bind(new Key<>(paramType, name), arg);
        }
      }

      Class<?> returnType = method.getReturnType();
      return subBuilder.build().getInstance(new Key<T>(returnType));
    }
  }

  private static class UnsatisfiedDependencyException extends RuntimeException {

    private final Key<?> key;
    private final UnsatisfiedDependencyException inner;

    UnsatisfiedDependencyException(Key<?> key, UnsatisfiedDependencyException inner) {
      super(key.toString());
      this.key = key;
      this.inner = inner;
    }

    <T> InjectionException asInjectionException(Key<T> key) {
      StringBuilder buf = new StringBuilder("Failed to resolve dependency: ");
      UnsatisfiedDependencyException current = this;
      while (current != null) {
        buf.append(current.key.toShortString());
        current = current.inner;
        if (current != null) {
          buf.append("/");
        }
      }
      return new InjectionException(key, buf.toString(), this);
    }
  }
}