• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    */
clickOn(View view)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    */
visualize(View view)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")
dump(View view)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")
innerText(View view)111   public static String innerText(View view) {
112     ShadowView shadowView = Shadow.extract(view);
113     return shadowView.innerText();
114   }
115 
116   @Implementation
__constructor__(Context context, AttributeSet attributeSet, int defStyle)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
setLayerType(int layerType, Paint paint)127   protected void setLayerType(int layerType, Paint paint) {
128     this.layerType = layerType;
129   }
130 
131   @Implementation
setOnFocusChangeListener(View.OnFocusChangeListener l)132   protected void setOnFocusChangeListener(View.OnFocusChangeListener l) {
133     onFocusChangeListener = l;
134     directly().setOnFocusChangeListener(l);
135   }
136 
137   @Implementation
setOnClickListener(View.OnClickListener onClickListener)138   protected void setOnClickListener(View.OnClickListener onClickListener) {
139     this.onClickListener = onClickListener;
140     directly().setOnClickListener(onClickListener);
141   }
142 
143   @Implementation
setOnLongClickListener(View.OnLongClickListener onLongClickListener)144   protected void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
145     this.onLongClickListener = onLongClickListener;
146     directly().setOnLongClickListener(onLongClickListener);
147   }
148 
149   @Implementation
setOnSystemUiVisibilityChangeListener( View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener)150   protected void setOnSystemUiVisibilityChangeListener(
151       View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener) {
152     this.onSystemUiVisibilityChangeListener = onSystemUiVisibilityChangeListener;
153     directly().setOnSystemUiVisibilityChangeListener(onSystemUiVisibilityChangeListener);
154   }
155 
156   @Implementation
setOnCreateContextMenuListener( View.OnCreateContextMenuListener onCreateContextMenuListener)157   protected void setOnCreateContextMenuListener(
158       View.OnCreateContextMenuListener onCreateContextMenuListener) {
159     this.onCreateContextMenuListener = onCreateContextMenuListener;
160     directly().setOnCreateContextMenuListener(onCreateContextMenuListener);
161   }
162 
163   @Implementation
draw(android.graphics.Canvas canvas)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
onLayout(boolean changed, int left, int top, int right, int bottom)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 
onLayoutWasCalled()184   public boolean onLayoutWasCalled() {
185     return onLayoutWasCalled;
186   }
187 
188   @Implementation
requestLayout()189   protected void requestLayout() {
190     didRequestLayout = true;
191     directly().requestLayout();
192   }
193 
didRequestLayout()194   public boolean didRequestLayout() {
195     return didRequestLayout;
196   }
197 
setDidRequestLayout(boolean didRequestLayout)198   public void setDidRequestLayout(boolean didRequestLayout) {
199     this.didRequestLayout = didRequestLayout;
200   }
201 
setViewFocus(boolean hasFocus)202   public void setViewFocus(boolean hasFocus) {
203     if (onFocusChangeListener != null) {
204       onFocusChangeListener.onFocusChange(realView, hasFocus);
205     }
206   }
207 
208   @Implementation
invalidate()209   protected void invalidate() {
210     wasInvalidated = true;
211     directly().invalidate();
212   }
213 
214   @Implementation
onTouchEvent(MotionEvent event)215   protected boolean onTouchEvent(MotionEvent event) {
216     lastTouchEvent = event;
217     return directly().onTouchEvent(event);
218   }
219 
220   @Implementation
setOnTouchListener(View.OnTouchListener onTouchListener)221   protected void setOnTouchListener(View.OnTouchListener onTouchListener) {
222     this.onTouchListener = onTouchListener;
223     directly().setOnTouchListener(onTouchListener);
224   }
225 
getLastTouchEvent()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    */
innerText()236   public String innerText() {
237     return "";
238   }
239 
240   /**
241    * Dumps the status of this {@code View} to {@code System.out}
242    */
dump()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    */
dump(PrintStream out, int indent)252   public void dump(PrintStream out, int indent) {
253     dumpFirstPart(out, indent);
254     out.println("/>");
255   }
256 
dumpFirstPart(PrintStream out, int indent)257   protected void dumpFirstPart(PrintStream out, int indent) {
258     dumpIndent(out, indent);
259 
260     out.print("<" + realView.getClass().getSimpleName());
261     dumpAttributes(out);
262   }
263 
dumpAttributes(PrintStream out)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 
dumpAttribute(PrintStream out, String name, String value)281   protected void dumpAttribute(PrintStream out, String name, String value) {
282     out.print(" " + name + "=\"" + (value == null ? null : TextUtils.htmlEncode(value)) + "\"");
283   }
284 
dumpIndent(PrintStream out, int indent)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    */
wasInvalidated()292   public boolean wasInvalidated() {
293     return wasInvalidated;
294   }
295 
296   /**
297    * Clears the wasInvalidated flag
298    */
clearWasInvalidated()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    */
checkedPerformClick()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    */
getOnTouchListener()324   public View.OnTouchListener getOnTouchListener() {
325     return onTouchListener;
326   }
327 
328   /**
329    * @return Returns click listener, if set.
330    */
getOnClickListener()331   public View.OnClickListener getOnClickListener() {
332     return onClickListener;
333   }
334 
335   /**
336    * @return Returns long click listener, if set.
337    */
getOnLongClickListener()338   public View.OnLongClickListener getOnLongClickListener() {
339     return onLongClickListener;
340   }
341 
342   /**
343    * @return Returns system ui visibility change listener.
344    */
getOnSystemUiVisibilityChangeListener()345   public View.OnSystemUiVisibilityChangeListener getOnSystemUiVisibilityChangeListener() {
346     return onSystemUiVisibilityChangeListener;
347   }
348 
349   /**
350    * @return Returns create ContextMenu listener, if set.
351    */
getOnCreateContextMenuListener()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
post(Runnable action)362   protected boolean post(Runnable action) {
363     ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
364     return true;
365   }
366 
367   @Implementation
postDelayed(Runnable action, long delayMills)368   protected boolean postDelayed(Runnable action, long delayMills) {
369     ShadowApplication.getInstance().getForegroundThreadScheduler().postDelayed(action, delayMills);
370     return true;
371   }
372 
373   @Implementation
postInvalidateDelayed(long delayMilliseconds)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
removeCallbacks(Runnable callback)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
scrollTo(int x, int y)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
scrollBy(int x, int y)405   protected void scrollBy(int x, int y) {
406     scrollTo(getScrollX() + x, getScrollY() + y);
407   }
408 
409   @Implementation
getScrollX()410   protected int getScrollX() {
411     return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
412   }
413 
414   @Implementation
getScrollY()415   protected int getScrollY() {
416     return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
417   }
418 
419   @Implementation
setScrollX(int scrollX)420   protected void setScrollX(int scrollX) {
421     scrollTo(scrollX, scrollToCoordinates.y);
422   }
423 
424   @Implementation
setScrollY(int scrollY)425   protected void setScrollY(int scrollY) {
426     scrollTo(scrollToCoordinates.x, scrollY);
427   }
428 
429   @Implementation
getLayerType()430   protected int getLayerType() {
431     return this.layerType;
432   }
433 
434   @Implementation
setAnimation(final Animation animation)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 
AnimationRunner(Animation animation)449     AnimationRunner(Animation animation) {
450       this.animation = animation;
451       start();
452     }
453 
start()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
run()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)
isAttachedToWindow()492   protected boolean isAttachedToWindow() {
493     return getAttachInfo() != null;
494   }
495 
getAttachInfo()496   private Object getAttachInfo() {
497     return getField(realView, "mAttachInfo");
498   }
499 
callOnAttachedToWindow()500   public void callOnAttachedToWindow() {
501     invokeReflectively("onAttachedToWindow");
502   }
503 
callOnDetachedFromWindow()504   public void callOnDetachedFromWindow() {
505     invokeReflectively("onDetachedFromWindow");
506   }
507 
508   @Implementation(minSdk = JELLY_BEAN_MR2)
getWindowId()509   protected WindowId getWindowId() {
510     return WindowIdHelper.getWindowId(this);
511   }
512 
invokeReflectively(String methodName)513   private void invokeReflectively(String methodName) {
514     ReflectionHelpers.callInstanceMethod(realView, methodName);
515   }
516 
517   @Implementation
performHapticFeedback(int hapticFeedbackType)518   protected boolean performHapticFeedback(int hapticFeedbackType) {
519     hapticFeedbackPerformed = hapticFeedbackType;
520     return true;
521   }
522 
523   @Implementation
getGlobalVisibleRect(Rect rect, Point globalOffset)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 
setGlobalVisibleRect(Rect rect)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 
lastHapticFeedbackPerformed()549   public int lastHapticFeedbackPerformed() {
550     return hapticFeedbackPerformed;
551   }
552 
setMyParent(ViewParent viewParent)553   public void setMyParent(ViewParent viewParent) {
554     directlyOn(realView, View.class, "assignParent", ClassParameter.from(ViewParent.class, viewParent));
555   }
556 
directly()557   private View directly() {
558     return directlyOn(realView, View.class);
559   }
560 
561   public static class WindowIdHelper {
getWindowId(ShadowView shadowView)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
registerFocusObserver(IWindowFocusObserver iWindowFocusObserver)577       public void registerFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
578       }
579 
580       @Override
unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver)581       public void unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
582       }
583 
584       @Override
isFocused()585       public boolean isFocused() throws RemoteException {
586         return true;
587       }
588     }
589   }
590 }
591