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

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

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);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy