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