Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
      4 import static android.os.Build.VERSION_CODES.KITKAT;
      5 import static org.robolectric.shadow.api.Shadow.directlyOn;
      6 import static org.robolectric.shadow.api.Shadow.invokeConstructor;
      7 import static org.robolectric.util.ReflectionHelpers.getField;
      8 import static org.robolectric.util.ReflectionHelpers.setField;
      9 
     10 import android.annotation.SuppressLint;
     11 import android.content.Context;
     12 import android.graphics.Canvas;
     13 import android.graphics.Paint;
     14 import android.graphics.Point;
     15 import android.graphics.Rect;
     16 import android.graphics.drawable.Drawable;
     17 import android.os.Looper;
     18 import android.os.RemoteException;
     19 import android.os.SystemClock;
     20 import android.text.TextUtils;
     21 import android.util.AttributeSet;
     22 import android.view.Choreographer;
     23 import android.view.IWindowFocusObserver;
     24 import android.view.IWindowId;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.view.ViewParent;
     28 import android.view.WindowId;
     29 import android.view.animation.Animation;
     30 import android.view.animation.Transformation;
     31 import java.io.PrintStream;
     32 import java.lang.reflect.Method;
     33 import org.robolectric.android.AccessibilityUtil;
     34 import org.robolectric.annotation.Implementation;
     35 import org.robolectric.annotation.Implements;
     36 import org.robolectric.annotation.RealObject;
     37 import org.robolectric.shadow.api.Shadow;
     38 import org.robolectric.util.ReflectionHelpers;
     39 import org.robolectric.util.ReflectionHelpers.ClassParameter;
     40 import org.robolectric.util.TimeUtils;
     41 
     42 @Implements(View.class)
     43 @SuppressLint("NewApi")
     44 public class ShadowView {
     45 
     46   @RealObject
     47   protected View realView;
     48 
     49   private View.OnClickListener onClickListener;
     50   private View.OnLongClickListener onLongClickListener;
     51   private View.OnFocusChangeListener onFocusChangeListener;
     52   private View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener;
     53   private boolean wasInvalidated;
     54   private View.OnTouchListener onTouchListener;
     55   protected AttributeSet attributeSet;
     56   public Point scrollToCoordinates = new Point();
     57   private boolean didRequestLayout;
     58   private MotionEvent lastTouchEvent;
     59   private float scaleX = 1.0f;
     60   private float scaleY = 1.0f;
     61   private int hapticFeedbackPerformed = -1;
     62   private boolean onLayoutWasCalled;
     63   private View.OnCreateContextMenuListener onCreateContextMenuListener;
     64   private Rect globalVisibleRect;
     65   private int layerType;
     66 
     67   /**
     68    * Calls {@code performClick()} on a {@code View} after ensuring that it and its ancestors are visible and that it
     69    * is enabled.
     70    *
     71    * @param view the view to click on
     72    * @return true if {@code View.OnClickListener}s were found and fired, false otherwise.
     73    * @throws RuntimeException if the preconditions are not met.
     74    */
     75   public static boolean clickOn(View view) {
     76     ShadowView shadowView = Shadow.extract(view);
     77     return shadowView.checkedPerformClick();
     78   }
     79 
     80   /**
     81    * Returns a textual representation of the appearance of the object.
     82    *
     83    * @param view the view to visualize
     84    * @return Textual representation of the appearance of the object.
     85    */
     86   public static String visualize(View view) {
     87     Canvas canvas = new Canvas();
     88     view.draw(canvas);
     89     ShadowCanvas shadowCanvas = Shadow.extract(canvas);
     90     return shadowCanvas.getDescription();
     91   }
     92 
     93   /**
     94    * Emits an xml-like representation of the view to System.out.
     95    *
     96    * @param view the view to dump
     97    */
     98   @SuppressWarnings("UnusedDeclaration")
     99   public static void dump(View view) {
    100     ShadowView shadowView = Shadow.extract(view);
    101     shadowView.dump();
    102   }
    103 
    104   /**
    105    * Returns the text contained within this view.
    106    *
    107    * @param view the view to scan for text
    108    * @return Text contained within this view.
    109    */
    110   @SuppressWarnings("UnusedDeclaration")
    111   public static String innerText(View view) {
    112     ShadowView shadowView = Shadow.extract(view);
    113     return shadowView.innerText();
    114   }
    115 
    116   @Implementation
    117   protected void __constructor__(Context context, AttributeSet attributeSet, int defStyle) {
    118     if (context == null) throw new NullPointerException("no context");
    119     this.attributeSet = attributeSet;
    120     invokeConstructor(View.class, realView,
    121         ClassParameter.from(Context.class, context),
    122         ClassParameter.from(AttributeSet.class, attributeSet),
    123         ClassParameter.from(int.class, defStyle));
    124   }
    125 
    126   @Implementation
    127   protected void setLayerType(int layerType, Paint paint) {
    128     this.layerType = layerType;
    129   }
    130 
    131   @Implementation
    132   protected void setOnFocusChangeListener(View.OnFocusChangeListener l) {
    133     onFocusChangeListener = l;
    134     directly().setOnFocusChangeListener(l);
    135   }
    136 
    137   @Implementation
    138   protected void setOnClickListener(View.OnClickListener onClickListener) {
    139     this.onClickListener = onClickListener;
    140     directly().setOnClickListener(onClickListener);
    141   }
    142 
    143   @Implementation
    144   protected void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
    145     this.onLongClickListener = onLongClickListener;
    146     directly().setOnLongClickListener(onLongClickListener);
    147   }
    148 
    149   @Implementation
    150   protected void setOnSystemUiVisibilityChangeListener(
    151       View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener) {
    152     this.onSystemUiVisibilityChangeListener = onSystemUiVisibilityChangeListener;
    153     directly().setOnSystemUiVisibilityChangeListener(onSystemUiVisibilityChangeListener);
    154   }
    155 
    156   @Implementation
    157   protected void setOnCreateContextMenuListener(
    158       View.OnCreateContextMenuListener onCreateContextMenuListener) {
    159     this.onCreateContextMenuListener = onCreateContextMenuListener;
    160     directly().setOnCreateContextMenuListener(onCreateContextMenuListener);
    161   }
    162 
    163   @Implementation
    164   protected void draw(android.graphics.Canvas canvas) {
    165     Drawable background = realView.getBackground();
    166     if (background != null) {
    167       ShadowCanvas shadowCanvas = Shadow.extract(canvas);
    168       shadowCanvas.appendDescription("background:");
    169       background.draw(canvas);
    170     }
    171   }
    172 
    173   @Implementation
    174   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    175     onLayoutWasCalled = true;
    176     directlyOn(realView, View.class, "onLayout",
    177         ClassParameter.from(boolean.class, changed),
    178         ClassParameter.from(int.class, left),
    179         ClassParameter.from(int.class, top),
    180         ClassParameter.from(int.class, right),
    181         ClassParameter.from(int.class, bottom));
    182   }
    183 
    184   public boolean onLayoutWasCalled() {
    185     return onLayoutWasCalled;
    186   }
    187 
    188   @Implementation
    189   protected void requestLayout() {
    190     didRequestLayout = true;
    191     directly().requestLayout();
    192   }
    193 
    194   public boolean didRequestLayout() {
    195     return didRequestLayout;
    196   }
    197 
    198   public void setDidRequestLayout(boolean didRequestLayout) {
    199     this.didRequestLayout = didRequestLayout;
    200   }
    201 
    202   public void setViewFocus(boolean hasFocus) {
    203     if (onFocusChangeListener != null) {
    204       onFocusChangeListener.onFocusChange(realView, hasFocus);
    205     }
    206   }
    207 
    208   @Implementation
    209   protected void invalidate() {
    210     wasInvalidated = true;
    211     directly().invalidate();
    212   }
    213 
    214   @Implementation
    215   protected boolean onTouchEvent(MotionEvent event) {
    216     lastTouchEvent = event;
    217     return directly().onTouchEvent(event);
    218   }
    219 
    220   @Implementation
    221   protected void setOnTouchListener(View.OnTouchListener onTouchListener) {
    222     this.onTouchListener = onTouchListener;
    223     directly().setOnTouchListener(onTouchListener);
    224   }
    225 
    226   public MotionEvent getLastTouchEvent() {
    227     return lastTouchEvent;
    228   }
    229 
    230   /**
    231    * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string.
    232    *
    233    * Robolectric extension.
    234    * @return String representation of this view.
    235    */
    236   public String innerText() {
    237     return "";
    238   }
    239 
    240   /**
    241    * Dumps the status of this {@code View} to {@code System.out}
    242    */
    243   public void dump() {
    244     dump(System.out, 0);
    245   }
    246 
    247   /**
    248    * Dumps the status of this {@code View} to {@code System.out} at the given indentation level
    249    * @param out Output stream.
    250    * @param indent Indentation level.
    251    */
    252   public void dump(PrintStream out, int indent) {
    253     dumpFirstPart(out, indent);
    254     out.println("/>");
    255   }
    256 
    257   protected void dumpFirstPart(PrintStream out, int indent) {
    258     dumpIndent(out, indent);
    259 
    260     out.print("<" + realView.getClass().getSimpleName());
    261     dumpAttributes(out);
    262   }
    263 
    264   protected void dumpAttributes(PrintStream out) {
    265     if (realView.getId() > 0) {
    266       dumpAttribute(out, "id", realView.getContext().getResources().getResourceName(realView.getId()));
    267     }
    268 
    269     switch (realView.getVisibility()) {
    270       case View.VISIBLE:
    271         break;
    272       case View.INVISIBLE:
    273         dumpAttribute(out, "visibility", "INVISIBLE");
    274         break;
    275       case View.GONE:
    276         dumpAttribute(out, "visibility", "GONE");
    277         break;
    278     }
    279   }
    280 
    281   protected void dumpAttribute(PrintStream out, String name, String value) {
    282     out.print(" " + name + "=\"" + (value == null ? null : TextUtils.htmlEncode(value)) + "\"");
    283   }
    284 
    285   protected void dumpIndent(PrintStream out, int indent) {
    286     for (int i = 0; i < indent; i++) out.print(" ");
    287   }
    288 
    289   /**
    290    * @return whether or not {@link #invalidate()} has been called
    291    */
    292   public boolean wasInvalidated() {
    293     return wasInvalidated;
    294   }
    295 
    296   /**
    297    * Clears the wasInvalidated flag
    298    */
    299   public void clearWasInvalidated() {
    300     wasInvalidated = false;
    301   }
    302 
    303   /**
    304    * Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app.
    305    *
    306    * @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible.
    307    * @return Return value of the underlying click operation.
    308    */
    309   public boolean checkedPerformClick() {
    310     if (!realView.isShown()) {
    311       throw new RuntimeException("View is not visible and cannot be clicked");
    312     }
    313     if (!realView.isEnabled()) {
    314       throw new RuntimeException("View is not enabled and cannot be clicked");
    315     }
    316 
    317     AccessibilityUtil.checkViewIfCheckingEnabled(realView);
    318     return realView.performClick();
    319   }
    320 
    321   /**
    322    * @return Touch listener, if set.
    323    */
    324   public View.OnTouchListener getOnTouchListener() {
    325     return onTouchListener;
    326   }
    327 
    328   /**
    329    * @return Returns click listener, if set.
    330    */
    331   public View.OnClickListener getOnClickListener() {
    332     return onClickListener;
    333   }
    334 
    335   /**
    336    * @return Returns long click listener, if set.
    337    */
    338   public View.OnLongClickListener getOnLongClickListener() {
    339     return onLongClickListener;
    340   }
    341 
    342   /**
    343    * @return Returns system ui visibility change listener.
    344    */
    345   public View.OnSystemUiVisibilityChangeListener getOnSystemUiVisibilityChangeListener() {
    346     return onSystemUiVisibilityChangeListener;
    347   }
    348 
    349   /**
    350    * @return Returns create ContextMenu listener, if set.
    351    */
    352   public View.OnCreateContextMenuListener getOnCreateContextMenuListener() {
    353     return onCreateContextMenuListener;
    354   }
    355 
    356   // @Implementation
    357   // protected Bitmap getDrawingCache() {
    358   //   return ReflectionHelpers.callConstructor(Bitmap.class);
    359   // }
    360 
    361   @Implementation
    362   protected boolean post(Runnable action) {
    363     ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
    364     return true;
    365   }
    366 
    367   @Implementation
    368   protected boolean postDelayed(Runnable action, long delayMills) {
    369     ShadowApplication.getInstance().getForegroundThreadScheduler().postDelayed(action, delayMills);
    370     return true;
    371   }
    372 
    373   @Implementation
    374   protected void postInvalidateDelayed(long delayMilliseconds) {
    375     ShadowApplication.getInstance().getForegroundThreadScheduler().postDelayed(new Runnable() {
    376       @Override
    377       public void run() {
    378         realView.invalidate();
    379       }
    380     }, delayMilliseconds);
    381   }
    382 
    383   @Implementation
    384   protected boolean removeCallbacks(Runnable callback) {
    385     ShadowLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
    386     shadowLooper.getScheduler().remove(callback);
    387     return true;
    388   }
    389 
    390   @Implementation
    391   protected void scrollTo(int x, int y) {
    392     try {
    393       Method method = View.class.getDeclaredMethod("onScrollChanged", new Class[]{int.class, int.class, int.class, int.class});
    394       method.setAccessible(true);
    395       method.invoke(realView, x, y, scrollToCoordinates.x, scrollToCoordinates.y);
    396     } catch (Exception e) {
    397       throw new RuntimeException(e);
    398     }
    399     scrollToCoordinates = new Point(x, y);
    400     ReflectionHelpers.setField(realView, "mScrollX", x);
    401     ReflectionHelpers.setField(realView, "mScrollY", y);
    402   }
    403 
    404   @Implementation
    405   protected void scrollBy(int x, int y) {
    406     scrollTo(getScrollX() + x, getScrollY() + y);
    407   }
    408 
    409   @Implementation
    410   protected int getScrollX() {
    411     return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
    412   }
    413 
    414   @Implementation
    415   protected int getScrollY() {
    416     return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
    417   }
    418 
    419   @Implementation
    420   protected void setScrollX(int scrollX) {
    421     scrollTo(scrollX, scrollToCoordinates.y);
    422   }
    423 
    424   @Implementation
    425   protected void setScrollY(int scrollY) {
    426     scrollTo(scrollToCoordinates.x, scrollY);
    427   }
    428 
    429   @Implementation
    430   protected int getLayerType() {
    431     return this.layerType;
    432   }
    433 
    434   @Implementation
    435   protected void setAnimation(final Animation animation) {
    436     directly().setAnimation(animation);
    437 
    438     if (animation != null) {
    439       new AnimationRunner(animation);
    440     }
    441   }
    442 
    443   private AnimationRunner animationRunner;
    444 
    445   private class AnimationRunner implements Runnable {
    446     private final Animation animation;
    447     private long startTime, startOffset, elapsedTime;
    448 
    449     AnimationRunner(Animation animation) {
    450       this.animation = animation;
    451       start();
    452     }
    453 
    454     private void start() {
    455       startTime = animation.getStartTime();
    456       startOffset = animation.getStartOffset();
    457       Choreographer choreographer = ShadowChoreographer.getInstance();
    458       if (animationRunner != null) {
    459         choreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, animationRunner, null);
    460       }
    461       animationRunner = this;
    462       int startDelay;
    463       if (startTime == Animation.START_ON_FIRST_FRAME) {
    464         startDelay = (int) startOffset;
    465       } else {
    466         startDelay = (int) ((startTime + startOffset) - SystemClock.uptimeMillis());
    467       }
    468       choreographer.postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, this, null, startDelay);
    469     }
    470 
    471     @Override
    472     public void run() {
    473       // Abort if start time has been messed with, as this simulation is only designed to handle
    474       // standard situations.
    475       if ((animation.getStartTime() == startTime && animation.getStartOffset() == startOffset) &&
    476           animation.getTransformation(startTime == Animation.START_ON_FIRST_FRAME ?
    477               SystemClock.uptimeMillis() : (startTime + startOffset + elapsedTime), new Transformation()) &&
    478               // We can't handle infinitely repeating animations in the current scheduling model,
    479               // so abort after one iteration.
    480               !(animation.getRepeatCount() == Animation.INFINITE && elapsedTime >= animation.getDuration())) {
    481         // Update startTime if it had a value of Animation.START_ON_FIRST_FRAME
    482         startTime = animation.getStartTime();
    483         elapsedTime += ShadowChoreographer.getFrameInterval() / TimeUtils.NANOS_PER_MS;
    484         ShadowChoreographer.getInstance().postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
    485       } else {
    486         animationRunner = null;
    487       }
    488     }
    489   }
    490 
    491   @Implementation(minSdk = KITKAT)
    492   protected boolean isAttachedToWindow() {
    493     return getAttachInfo() != null;
    494   }
    495 
    496   private Object getAttachInfo() {
    497     return getField(realView, "mAttachInfo");
    498   }
    499 
    500   public void callOnAttachedToWindow() {
    501     invokeReflectively("onAttachedToWindow");
    502   }
    503 
    504   public void callOnDetachedFromWindow() {
    505     invokeReflectively("onDetachedFromWindow");
    506   }
    507 
    508   @Implementation(minSdk = JELLY_BEAN_MR2)
    509   protected WindowId getWindowId() {
    510     return WindowIdHelper.getWindowId(this);
    511   }
    512 
    513   private void invokeReflectively(String methodName) {
    514     ReflectionHelpers.callInstanceMethod(realView, methodName);
    515   }
    516 
    517   @Implementation
    518   protected boolean performHapticFeedback(int hapticFeedbackType) {
    519     hapticFeedbackPerformed = hapticFeedbackType;
    520     return true;
    521   }
    522 
    523   @Implementation
    524   protected boolean getGlobalVisibleRect(Rect rect, Point globalOffset) {
    525     if (globalVisibleRect == null) {
    526       return directly().getGlobalVisibleRect(rect, globalOffset);
    527     }
    528 
    529     if (!globalVisibleRect.isEmpty()) {
    530       rect.set(globalVisibleRect);
    531       if (globalOffset != null) {
    532         rect.offset(-globalOffset.x, -globalOffset.y);
    533       }
    534       return true;
    535     }
    536     rect.setEmpty();
    537     return false;
    538   }
    539 
    540   public void setGlobalVisibleRect(Rect rect) {
    541     if (rect != null) {
    542       globalVisibleRect = new Rect();
    543       globalVisibleRect.set(rect);
    544     } else {
    545       globalVisibleRect = null;
    546     }
    547   }
    548 
    549   public int lastHapticFeedbackPerformed() {
    550     return hapticFeedbackPerformed;
    551   }
    552 
    553   public void setMyParent(ViewParent viewParent) {
    554     directlyOn(realView, View.class, "assignParent", ClassParameter.from(ViewParent.class, viewParent));
    555   }
    556 
    557   private View directly() {
    558     return directlyOn(realView, View.class);
    559   }
    560 
    561   public static class WindowIdHelper {
    562     public static WindowId getWindowId(ShadowView shadowView) {
    563       if (shadowView.isAttachedToWindow()) {
    564         Object attachInfo = shadowView.getAttachInfo();
    565         if (getField(attachInfo, "mWindowId") == null) {
    566           IWindowId iWindowId = new MyIWindowIdStub();
    567           setField(attachInfo, "mWindowId", new WindowId(iWindowId));
    568           setField(attachInfo, "mIWindowId", iWindowId);
    569         }
    570       }
    571 
    572       return shadowView.directly().getWindowId();
    573     }
    574 
    575     private static class MyIWindowIdStub extends IWindowId.Stub {
    576       @Override
    577       public void registerFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
    578       }
    579 
    580       @Override
    581       public void unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
    582       }
    583 
    584       @Override
    585       public boolean isFocused() throws RemoteException {
    586         return true;
    587       }
    588     }
    589   }
    590 }
    591