1 package com.xtremelabs.robolectric.shadows;
2 
3 import static com.xtremelabs.robolectric.Robolectric.shadowOf;
4 import static com.xtremelabs.robolectric.Robolectric.Reflection.newInstanceOf;
5 
6 import android.content.Context;
7 import android.content.res.Resources;
8 import android.graphics.Bitmap;
9 import android.graphics.Point;
10 import android.graphics.drawable.ColorDrawable;
11 import android.graphics.drawable.Drawable;
12 import android.util.AttributeSet;
13 import android.view.KeyEvent;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.view.View.MeasureSpec;
17 import android.view.ViewGroup;
18 import android.view.ViewParent;
19 import android.view.ViewTreeObserver;
20 import android.view.animation.Animation;
21 
22 import com.xtremelabs.robolectric.Robolectric;
23 import com.xtremelabs.robolectric.internal.Implementation;
24 import com.xtremelabs.robolectric.internal.Implements;
25 import com.xtremelabs.robolectric.internal.RealObject;
26 
27 import java.io.PrintStream;
28 import java.lang.reflect.InvocationTargetException;
29 import java.lang.reflect.Method;
30 import java.util.HashMap;
31 import java.util.Map;
32 
33 /**
34  * Shadow implementation of {@code View} that simulates the behavior of this
35  * class.
36  * <p/>
37  * Supports listeners, focusability (but not focus order), resource loading,
38  * visibility, onclick, tags, and tracks the size and shape of the view.
39  */
40 @SuppressWarnings({"UnusedDeclaration"})
41 @Implements(View.class)
42 public class ShadowView {
43     @RealObject
44     protected View realView;
45 
46     private int id;
47     ShadowView parent;
48     protected Context context;
49     private boolean selected;
50     private View.OnClickListener onClickListener;
51     private View.OnLongClickListener onLongClickListener;
52     private Object tag;
53     private boolean enabled = true;
54     private int visibility = View.VISIBLE;
55     private boolean filterTouchesWhenObscured = false;
56     int left;
57     int top;
58     int right;
59     int bottom;
60     private int paddingLeft;
61     private int paddingTop;
62     private int paddingRight;
63     private int paddingBottom;
64     private ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(0, 0);
65     private final Map<Integer, Object> tags = new HashMap<Integer, Object>();
66     private boolean clickable;
67     protected boolean focusable;
68     boolean focusableInTouchMode;
69     private int backgroundResourceId = -1;
70     private int backgroundColor;
71     protected View.OnKeyListener onKeyListener;
72     private boolean isFocused;
73     private View.OnFocusChangeListener onFocusChangeListener;
74     private boolean wasInvalidated;
75     private View.OnTouchListener onTouchListener;
76     protected AttributeSet attributeSet;
77     private boolean drawingCacheEnabled;
78     public Point scrollToCoordinates;
79     private boolean didRequestLayout;
80     private Drawable background;
81     private Animation animation;
82     private ViewTreeObserver viewTreeObserver;
83     private MotionEvent lastTouchEvent;
84     private int nextFocusDownId = View.NO_ID;
85     private CharSequence contentDescription = null;
86     private int measuredWidth = 0;
87     private int measuredHeight = 0;
88 
__constructor__(Context context)89     public void __constructor__(Context context) {
90         __constructor__(context, null);
91     }
92 
__constructor__(Context context, AttributeSet attributeSet)93     public void __constructor__(Context context, AttributeSet attributeSet) {
94         __constructor__(context, attributeSet, 0);
95     }
96 
__constructor__(Context context, AttributeSet attributeSet, int defStyle)97     public void __constructor__(Context context, AttributeSet attributeSet, int defStyle) {
98         this.context = context;
99         this.attributeSet = attributeSet;
100 
101         if (attributeSet != null) {
102             applyAttributes();
103         }
104     }
105 
applyAttributes()106     public void applyAttributes() {
107         applyIdAttribute();
108         applyVisibilityAttribute();
109         applyFilterTouchesWhenObscuredAttribute();
110         applyClickableAttribute();
111         applyFocusableAttribute();
112         applyEnabledAttribute();
113         applyBackgroundAttribute();
114         applyTagAttribute();
115         applyOnClickAttribute();
116         applyContentDescriptionAttribute();
117     }
118 
119     @Implementation
setId(int id)120     public void setId(int id) {
121         this.id = id;
122     }
123 
124     @Implementation
setClickable(boolean clickable)125     public void setClickable(boolean clickable) {
126         this.clickable = clickable;
127     }
128 
129     /**
130      * Also sets focusable in touch mode to false if {@code focusable} is false, which is the Android behavior.
131      *
132      * @param focusable the new status of the {@code View}'s focusability
133      */
134     @Implementation
setFocusable(boolean focusable)135     public void setFocusable(boolean focusable) {
136         this.focusable = focusable;
137         if (!focusable) {
138             setFocusableInTouchMode(false);
139         }
140     }
141 
142     @Implementation
isFocusableInTouchMode()143     public final boolean isFocusableInTouchMode() {
144         return focusableInTouchMode;
145     }
146 
147     /**
148      * Also sets focusable to true if {@code focusableInTouchMode} is true, which is the Android behavior.
149      *
150      * @param focusableInTouchMode the new status of the {@code View}'s touch mode focusability
151      */
152     @Implementation
setFocusableInTouchMode(boolean focusableInTouchMode)153     public void setFocusableInTouchMode(boolean focusableInTouchMode) {
154         this.focusableInTouchMode = focusableInTouchMode;
155         if (focusableInTouchMode) {
156             setFocusable(true);
157         }
158     }
159 
160     @Implementation(i18nSafe = false)
setContentDescription(CharSequence contentDescription)161     public void setContentDescription(CharSequence contentDescription) {
162         this.contentDescription = contentDescription;
163     }
164 
165     @Implementation
isFocusable()166     public boolean isFocusable() {
167         return focusable;
168     }
169 
170     @Implementation
getId()171     public int getId() {
172         return id;
173     }
174 
175     @Implementation
getContentDescription()176     public CharSequence getContentDescription() {
177         return contentDescription;
178     }
179 
180     /**
181      * Simulates the inflating of the requested resource.
182      *
183      * @param context  the context from which to obtain a layout inflater
184      * @param resource the ID of the resource to inflate
185      * @param root     the {@code ViewGroup} to add the inflated {@code View} to
186      * @return the inflated View
187      */
188     @Implementation
inflate(Context context, int resource, ViewGroup root)189     public static View inflate(Context context, int resource, ViewGroup root) {
190         return ShadowLayoutInflater.from(context).inflate(resource, root);
191     }
192 
193     /**
194      * Finds this {@code View} if it's ID is passed in, returns {@code null} otherwise
195      *
196      * @param id the id of the {@code View} to find
197      * @return the {@code View}, if found, {@code null} otherwise
198      */
199     @Implementation
findViewById(int id)200     public View findViewById(int id) {
201         if (id == this.id) {
202             return realView;
203         }
204 
205         return null;
206     }
207 
208     @Implementation
findViewWithTag(Object obj)209     public View findViewWithTag(Object obj) {
210         if (obj.equals(realView.getTag())) {
211             return realView;
212         }
213 
214         return null;
215     }
216 
217     @Implementation
getRootView()218     public View getRootView() {
219         ShadowView root = this;
220         while (root.parent != null) {
221             root = root.parent;
222         }
223         return root.realView;
224     }
225 
226     @Implementation
getLayoutParams()227     public ViewGroup.LayoutParams getLayoutParams() {
228         return layoutParams;
229     }
230 
231     @Implementation
setLayoutParams(ViewGroup.LayoutParams params)232     public void setLayoutParams(ViewGroup.LayoutParams params) {
233         layoutParams = params;
234     }
235 
236     @Implementation
getParent()237     public final ViewParent getParent() {
238         return parent == null ? null : (ViewParent) parent.realView;
239     }
240 
241     @Implementation
getContext()242     public final Context getContext() {
243         return context;
244     }
245 
246     @Implementation
getResources()247     public Resources getResources() {
248         return context.getResources();
249     }
250 
251     @Implementation
setBackgroundResource(int backgroundResourceId)252     public void setBackgroundResource(int backgroundResourceId) {
253         this.backgroundResourceId = backgroundResourceId;
254         setBackgroundDrawable(getResources().getDrawable(backgroundResourceId));
255     }
256 
257     /**
258      * Non-Android accessor.
259      *
260      * @return the resource ID of this views background
261      */
getBackgroundResourceId()262     public int getBackgroundResourceId() {
263         return backgroundResourceId;
264     }
265 
266     @Implementation
setBackgroundColor(int color)267     public void setBackgroundColor(int color) {
268         backgroundColor = color;
269         setBackgroundDrawable(new ColorDrawable(getResources().getColor(color)));
270     }
271 
272     /**
273      * Non-Android accessor.
274      *
275      * @return the resource color ID of this views background
276      */
getBackgroundColor()277     public int getBackgroundColor() {
278         return backgroundColor;
279     }
280 
281     @Implementation
setBackgroundDrawable(Drawable d)282     public void setBackgroundDrawable(Drawable d) {
283         this.background = d;
284     }
285 
286     @Implementation
getBackground()287     public Drawable getBackground() {
288         return background;
289     }
290 
291     @Implementation
getVisibility()292     public int getVisibility() {
293         return visibility;
294     }
295 
296     @Implementation
setVisibility(int visibility)297     public void setVisibility(int visibility) {
298         this.visibility = visibility;
299     }
300 
301     @Implementation
getFilterTouchesWhenObscured()302     public boolean getFilterTouchesWhenObscured() {
303         return filterTouchesWhenObscured;
304     }
305 
306     @Implementation
setFilterTouchesWhenObscured(boolean enabled)307     public void setFilterTouchesWhenObscured(boolean enabled) {
308         this.filterTouchesWhenObscured = enabled;
309     }
310 
311     @Implementation
setSelected(boolean selected)312     public void setSelected(boolean selected) {
313         this.selected = selected;
314     }
315 
316     @Implementation
isSelected()317     public boolean isSelected() {
318         return this.selected;
319     }
320 
321     @Implementation
isEnabled()322     public boolean isEnabled() {
323         return this.enabled;
324     }
325 
326     @Implementation
setEnabled(boolean enabled)327     public void setEnabled(boolean enabled) {
328         this.enabled = enabled;
329     }
330 
331     @Implementation
setOnClickListener(View.OnClickListener onClickListener)332     public void setOnClickListener(View.OnClickListener onClickListener) {
333         this.onClickListener = onClickListener;
334     }
335 
336     @Implementation
performClick()337     public boolean performClick() {
338         if (onClickListener != null) {
339             onClickListener.onClick(realView);
340             return true;
341         } else {
342             return false;
343         }
344     }
345 
346     @Implementation
setOnLongClickListener(View.OnLongClickListener onLongClickListener)347     public void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
348         this.onLongClickListener = onLongClickListener;
349     }
350 
351     @Implementation
performLongClick()352     public boolean performLongClick() {
353         if (onLongClickListener != null) {
354             onLongClickListener.onLongClick(realView);
355             return true;
356         } else {
357             return false;
358         }
359     }
360 
361     @Implementation
setOnKeyListener(View.OnKeyListener onKeyListener)362     public void setOnKeyListener(View.OnKeyListener onKeyListener) {
363         this.onKeyListener = onKeyListener;
364     }
365 
366     @Implementation
getTag()367     public Object getTag() {
368         return this.tag;
369     }
370 
371     @Implementation
setTag(Object tag)372     public void setTag(Object tag) {
373         this.tag = tag;
374     }
375 
376     @Implementation
getHeight()377     public final int getHeight() {
378         return bottom - top;
379     }
380 
381     @Implementation
getWidth()382     public final int getWidth() {
383         return right - left;
384     }
385 
386     @Implementation
getMeasuredWidth()387     public final int getMeasuredWidth() {
388         return measuredWidth;
389     }
390 
391     @Implementation
getMeasuredHeight()392     public final int getMeasuredHeight() {
393         return measuredHeight;
394     }
395 
396     @Implementation
setMeasuredDimension(int measuredWidth, int measuredHeight)397     public final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
398     	this.measuredWidth = measuredWidth;
399     	this.measuredHeight = measuredHeight;
400     }
401 
402     @Implementation
onMeasure(int widthMeasureSpec, int heightMeasureSpec)403     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
404     	setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
405     			MeasureSpec.getSize(heightMeasureSpec));
406     }
407 
408     @Implementation
measure(int widthMeasureSpec, int heightMeasureSpec)409     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
410     	// We really want to invoke the onMeasure method of the real view,
411     	// as the real View likely contains an implementation of onMeasure
412     	// worthy of test, rather the default shadow implementation.
413     	// But Android declares onMeasure as protected.
414     	try {
415     		Method onMeasureMethod = realView.getClass().getDeclaredMethod("onMeasure", Integer.TYPE, Integer.TYPE );
416     		onMeasureMethod.setAccessible(true);
417     		onMeasureMethod.invoke( realView, widthMeasureSpec, heightMeasureSpec );
418     	} catch ( NoSuchMethodException e ) {
419     		// use default shadow implementation
420     		onMeasure(widthMeasureSpec, heightMeasureSpec);
421     	} catch ( IllegalAccessException e ) {
422     		throw new RuntimeException(e);
423     	} catch ( InvocationTargetException e ) {
424     		throw new RuntimeException(e);
425     	}
426     }
427 
428     @Implementation
layout(int l, int t, int r, int b)429     public final void layout(int l, int t, int r, int b) {
430         left = l;
431         top = t;
432         right = r;
433         bottom = b;
434 // todo:       realView.onLayout();
435     }
436 
437     @Implementation
setPadding(int left, int top, int right, int bottom)438     public void setPadding(int left, int top, int right, int bottom) {
439         paddingLeft = left;
440         paddingTop = top;
441         paddingRight = right;
442         paddingBottom = bottom;
443     }
444 
445     @Implementation
getPaddingTop()446     public int getPaddingTop() {
447         return paddingTop;
448     }
449 
450     @Implementation
getPaddingLeft()451     public int getPaddingLeft() {
452         return paddingLeft;
453     }
454 
455     @Implementation
getPaddingRight()456     public int getPaddingRight() {
457         return paddingRight;
458     }
459 
460     @Implementation
getPaddingBottom()461     public int getPaddingBottom() {
462         return paddingBottom;
463     }
464 
465     @Implementation
getTag(int key)466     public Object getTag(int key) {
467         return tags.get(key);
468     }
469 
470     @Implementation
setTag(int key, Object value)471     public void setTag(int key, Object value) {
472         tags.put(key, value);
473     }
474 
475     @Implementation
requestLayout()476     public void requestLayout() {
477         didRequestLayout = true;
478     }
479 
didRequestLayout()480     public boolean didRequestLayout() {
481         return didRequestLayout;
482     }
483 
484     @Implementation
requestFocus()485     public final boolean requestFocus() {
486         return requestFocus(View.FOCUS_DOWN);
487     }
488 
489     @Implementation
requestFocus(int direction)490     public final boolean requestFocus(int direction) {
491         setViewFocus(true);
492         return true;
493     }
494 
setViewFocus(boolean hasFocus)495     public void setViewFocus(boolean hasFocus) {
496         this.isFocused = hasFocus;
497 
498         try {
499             Class rectClass = Class.forName("android.graphics.Rect");
500             Method method = View.class.getDeclaredMethod("onFocusChanged", Boolean.TYPE, Integer.TYPE,
501                 rectClass);
502             method.setAccessible(true);
503             method.invoke(realView, this.isFocused, 0, null);
504         } catch (IllegalAccessException e) {
505             throw new RuntimeException(e);
506         } catch (InvocationTargetException e) {
507             throw new RuntimeException(e);
508         } catch (NoSuchMethodException e) {
509             throw new RuntimeException(e);
510         } catch (ClassNotFoundException e) {
511             throw new RuntimeException(e);
512         }
513 
514         if (onFocusChangeListener != null) {
515             onFocusChangeListener.onFocusChange(realView, hasFocus);
516         }
517     }
518 
519     @Implementation
getNextFocusDownId()520     public int getNextFocusDownId() {
521         return nextFocusDownId;
522     }
523 
524     @Implementation
setNextFocusDownId(int nextFocusDownId)525     public void setNextFocusDownId(int nextFocusDownId) {
526         this.nextFocusDownId = nextFocusDownId;
527     }
528 
529     @Implementation
isFocused()530     public boolean isFocused() {
531         return isFocused;
532     }
533 
534     @Implementation
hasFocus()535     public boolean hasFocus() {
536         return isFocused;
537     }
538 
539     @Implementation
clearFocus()540     public void clearFocus() {
541         setViewFocus(false);
542     }
543 
544     @Implementation
findFocus()545     public View findFocus() {
546         return hasFocus() ? realView : null;
547     }
548 
549     @Implementation
setOnFocusChangeListener(View.OnFocusChangeListener listener)550     public void setOnFocusChangeListener(View.OnFocusChangeListener listener) {
551         onFocusChangeListener = listener;
552     }
553 
554     @Implementation
getOnFocusChangeListener()555     public View.OnFocusChangeListener getOnFocusChangeListener() {
556         return onFocusChangeListener;
557     }
558 
559     @Implementation
invalidate()560     public void invalidate() {
561         wasInvalidated = true;
562     }
563 
564     @Implementation
onTouchEvent(MotionEvent event)565     public boolean onTouchEvent(MotionEvent event) {
566         lastTouchEvent = event;
567         return false;
568     }
569 
570     @Implementation
setOnTouchListener(View.OnTouchListener onTouchListener)571     public void setOnTouchListener(View.OnTouchListener onTouchListener) {
572         this.onTouchListener = onTouchListener;
573     }
574 
575     @Implementation
dispatchTouchEvent(MotionEvent event)576     public boolean dispatchTouchEvent(MotionEvent event) {
577         if (onTouchListener != null && onTouchListener.onTouch(realView, event)) {
578             return true;
579         }
580         return realView.onTouchEvent(event);
581     }
582 
getLastTouchEvent()583     public MotionEvent getLastTouchEvent() {
584         return lastTouchEvent;
585     }
586 
587     @Implementation
dispatchKeyEvent(KeyEvent event)588     public boolean dispatchKeyEvent(KeyEvent event) {
589         if (onKeyListener != null) {
590             return onKeyListener.onKey(realView, event.getKeyCode(), event);
591         }
592         return false;
593     }
594 
595     /**
596      * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string.
597      * <p/>
598      * Robolectric extension.
599      */
innerText()600     public String innerText() {
601         return "";
602     }
603 
604     /**
605      * Dumps the status of this {@code View} to {@code System.out}
606      */
dump()607     public void dump() {
608         dump(System.out, 0);
609     }
610 
611     /**
612      * Dumps the status of this {@code View} to {@code System.out} at the given indentation level
613      */
dump(PrintStream out, int indent)614     public void dump(PrintStream out, int indent) {
615         dumpFirstPart(out, indent);
616         out.println("/>");
617     }
618 
dumpFirstPart(PrintStream out, int indent)619     protected void dumpFirstPart(PrintStream out, int indent) {
620         dumpIndent(out, indent);
621 
622         out.print("<" + realView.getClass().getSimpleName());
623         if (id > 0) {
624             out.print(" id=\"" + shadowOf(context).getResourceLoader().getNameForId(id) + "\"");
625         }
626     }
627 
dumpIndent(PrintStream out, int indent)628     protected void dumpIndent(PrintStream out, int indent) {
629         for (int i = 0; i < indent; i++) out.print(" ");
630     }
631 
632     /**
633      * @return left side of the view
634      */
635     @Implementation
getLeft()636     public int getLeft() {
637         return left;
638     }
639 
640     /**
641      * @return top coordinate of the view
642      */
643     @Implementation
getTop()644     public int getTop() {
645         return top;
646     }
647 
648     /**
649      * @return right side of the view
650      */
651     @Implementation
getRight()652     public int getRight() {
653         return right;
654     }
655 
656     /**
657      * @return bottom coordinate of the view
658      */
659     @Implementation
getBottom()660     public int getBottom() {
661         return bottom;
662     }
663 
664     /**
665      * @return whether the view is clickable
666      */
667     @Implementation
isClickable()668     public boolean isClickable() {
669         return clickable;
670     }
671 
672     /**
673      * Non-Android accessor.
674      *
675      * @return whether or not {@link #invalidate()} has been called
676      */
wasInvalidated()677     public boolean wasInvalidated() {
678         return wasInvalidated;
679     }
680 
681     /**
682      * Clears the wasInvalidated flag
683      */
clearWasInvalidated()684     public void clearWasInvalidated() {
685         wasInvalidated = false;
686     }
687 
688     @Implementation
setLeft(int left)689     public void setLeft(int left) {
690         this.left = left;
691     }
692 
693     @Implementation
setTop(int top)694     public void setTop(int top) {
695         this.top = top;
696     }
697 
698     @Implementation
setRight(int right)699     public void setRight(int right) {
700         this.right = right;
701     }
702 
703     @Implementation
setBottom(int bottom)704     public void setBottom(int bottom) {
705         this.bottom = bottom;
706     }
707 
708     /**
709      * Non-Android accessor.
710      */
setPaddingLeft(int paddingLeft)711     public void setPaddingLeft(int paddingLeft) {
712         this.paddingLeft = paddingLeft;
713     }
714 
715     /**
716      * Non-Android accessor.
717      */
setPaddingTop(int paddingTop)718     public void setPaddingTop(int paddingTop) {
719         this.paddingTop = paddingTop;
720     }
721 
722     /**
723      * Non-Android accessor.
724      */
setPaddingRight(int paddingRight)725     public void setPaddingRight(int paddingRight) {
726         this.paddingRight = paddingRight;
727     }
728 
729     /**
730      * Non-Android accessor.
731      */
setPaddingBottom(int paddingBottom)732     public void setPaddingBottom(int paddingBottom) {
733         this.paddingBottom = paddingBottom;
734     }
735 
736     /**
737      * Non-Android accessor.
738      */
setFocused(boolean focused)739     public void setFocused(boolean focused) {
740         isFocused = focused;
741     }
742 
743     /**
744      * Non-Android accessor.
745      *
746      * @return true if this object and all of its ancestors are {@code View.VISIBLE}, returns false if this or
747      *         any ancestor is not {@code View.VISIBLE}
748      */
derivedIsVisible()749     public boolean derivedIsVisible() {
750         View parent = realView;
751         while (parent != null) {
752             if (parent.getVisibility() != View.VISIBLE) {
753                 return false;
754             }
755             parent = (View) parent.getParent();
756         }
757         return true;
758     }
759 
760     /**
761      * Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app.
762      *
763      * @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible.
764      */
checkedPerformClick()765     public boolean checkedPerformClick() {
766         if (!derivedIsVisible()) {
767             throw new RuntimeException("View is not visible and cannot be clicked");
768         }
769         if (!realView.isEnabled()) {
770             throw new RuntimeException("View is not enabled and cannot be clicked");
771         }
772 
773         return realView.performClick();
774     }
775 
applyFocus()776     public void applyFocus() {
777         if (noParentHasFocus(realView)) {
778             Boolean focusRequested = attributeSet.getAttributeBooleanValue("android", "focus", false);
779             if (focusRequested || realView.isFocusableInTouchMode()) {
780                 realView.requestFocus();
781             }
782         }
783     }
784 
applyIdAttribute()785     private void applyIdAttribute() {
786         Integer id = attributeSet.getAttributeResourceValue("android", "id", 0);
787         if (getId() == 0) {
788             setId(id);
789         }
790     }
791 
applyTagAttribute()792     private void applyTagAttribute() {
793         Object tag = attributeSet.getAttributeValue("android", "tag");
794         if (tag != null) {
795             setTag(tag);
796         }
797     }
798 
applyVisibilityAttribute()799     private void applyVisibilityAttribute() {
800         String visibility = attributeSet.getAttributeValue("android", "visibility");
801         if (visibility != null) {
802             if (visibility.equals("gone")) {
803                 setVisibility(View.GONE);
804             } else if (visibility.equals("invisible")) {
805                 setVisibility(View.INVISIBLE);
806             }
807         }
808     }
809 
applyFilterTouchesWhenObscuredAttribute()810     private void applyFilterTouchesWhenObscuredAttribute() {
811         setFilterTouchesWhenObscured(attributeSet.getAttributeBooleanValue(
812                 "android", "filterTouchesWhenObscured", false));
813     }
814 
applyClickableAttribute()815     private void applyClickableAttribute() {
816         setClickable(attributeSet.getAttributeBooleanValue("android", "clickable", false));
817     }
818 
applyFocusableAttribute()819     private void applyFocusableAttribute() {
820         setFocusable(attributeSet.getAttributeBooleanValue("android", "focusable", false));
821     }
822 
applyEnabledAttribute()823     private void applyEnabledAttribute() {
824         setEnabled(attributeSet.getAttributeBooleanValue("android", "enabled", true));
825     }
826 
applyBackgroundAttribute()827     private void applyBackgroundAttribute() {
828         String source = attributeSet.getAttributeValue("android", "background");
829         if (source != null) {
830             if (source.startsWith("@drawable/")) {
831                 setBackgroundResource(attributeSet.getAttributeResourceValue("android", "background", 0));
832             }
833         }
834     }
835 
applyOnClickAttribute()836     private void applyOnClickAttribute() {
837         final String handlerName = attributeSet.getAttributeValue("android",
838                 "onClick");
839         if (handlerName == null) {
840             return;
841         }
842 
843         /* good part of following code has been directly copied from original
844          * android source */
845         setOnClickListener(new View.OnClickListener() {
846             @Override
847             public void onClick(View v) {
848                 Method mHandler;
849                 try {
850                     mHandler = getContext().getClass().getMethod(handlerName,
851                             View.class);
852                 } catch (NoSuchMethodException e) {
853                     int id = getId();
854                     String idText = id == View.NO_ID ? "" : " with id '"
855                             + shadowOf(context).getResourceLoader()
856                             .getNameForId(id) + "'";
857                     throw new IllegalStateException("Could not find a method " +
858                             handlerName + "(View) in the activity "
859                             + getContext().getClass() + " for onClick handler"
860                             + " on view " + realView.getClass() + idText, e);
861                 }
862 
863                 try {
864                     mHandler.invoke(getContext(), realView);
865                 } catch (IllegalAccessException e) {
866                     throw new IllegalStateException("Could not execute non "
867                             + "public method of the activity", e);
868                 } catch (InvocationTargetException e) {
869                     throw new IllegalStateException("Could not execute "
870                             + "method of the activity", e);
871                 }
872             }
873         });
874     }
875 
applyContentDescriptionAttribute()876     private void applyContentDescriptionAttribute() {
877         String contentDescription = attributeSet.getAttributeValue("android", "contentDescription");
878         if (contentDescription != null) {
879             if (contentDescription.startsWith("@string/")) {
880                 int resId = attributeSet.getAttributeResourceValue("android", "contentDescription", 0);
881                 contentDescription = context.getResources().getString(resId);
882             }
883             setContentDescription(contentDescription);
884         }
885     }
886 
noParentHasFocus(View view)887     private boolean noParentHasFocus(View view) {
888         while (view != null) {
889             if (view.hasFocus()) return false;
890             view = (View) view.getParent();
891         }
892         return true;
893     }
894 
895     /**
896      * Non-android accessor.  Returns touch listener, if set.
897      *
898      * @return
899      */
getOnTouchListener()900     public View.OnTouchListener getOnTouchListener() {
901         return onTouchListener;
902     }
903 
904     /**
905      * Non-android accessor.  Returns click listener, if set.
906      *
907      * @return
908      */
getOnClickListener()909     public View.OnClickListener getOnClickListener() {
910         return onClickListener;
911     }
912 
913     @Implementation
setDrawingCacheEnabled(boolean drawingCacheEnabled)914     public void setDrawingCacheEnabled(boolean drawingCacheEnabled) {
915         this.drawingCacheEnabled = drawingCacheEnabled;
916     }
917 
918     @Implementation
isDrawingCacheEnabled()919     public boolean isDrawingCacheEnabled() {
920         return drawingCacheEnabled;
921     }
922 
923     @Implementation
getDrawingCache()924     public Bitmap getDrawingCache() {
925         return Robolectric.newInstanceOf(Bitmap.class);
926     }
927 
928     @Implementation
post(Runnable action)929     public void post(Runnable action) {
930         Robolectric.getUiThreadScheduler().post(action);
931     }
932 
933     @Implementation
postDelayed(Runnable action, long delayMills)934     public void postDelayed(Runnable action, long delayMills) {
935         Robolectric.getUiThreadScheduler().postDelayed(action, delayMills);
936     }
937 
938     @Implementation
postInvalidateDelayed(long delayMilliseconds)939     public void postInvalidateDelayed(long delayMilliseconds) {
940         Robolectric.getUiThreadScheduler().postDelayed(new Runnable() {
941             @Override
942             public void run() {
943                 realView.invalidate();
944             }
945         }, delayMilliseconds);
946     }
947 
948     @Implementation
getAnimation()949     public Animation getAnimation() {
950         return animation;
951     }
952 
953     @Implementation
setAnimation(Animation anim)954     public void setAnimation(Animation anim) {
955         animation = anim;
956     }
957 
958     @Implementation
startAnimation(Animation anim)959     public void startAnimation(Animation anim) {
960         setAnimation(anim);
961         animation.start();
962     }
963 
964     @Implementation
clearAnimation()965     public void clearAnimation() {
966         if (animation != null) {
967             animation.cancel();
968             animation = null;
969         }
970     }
971 
972     @Implementation
scrollTo(int x, int y)973     public void scrollTo(int x, int y) {
974         this.scrollToCoordinates = new Point(x, y);
975     }
976 
977     @Implementation
getScrollX()978     public int getScrollX() {
979         return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
980     }
981 
982     @Implementation
getScrollY()983     public int getScrollY() {
984         return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
985     }
986 
987     @Implementation
getViewTreeObserver()988     public ViewTreeObserver getViewTreeObserver() {
989         if (viewTreeObserver == null) {
990             viewTreeObserver = newInstanceOf(ViewTreeObserver.class);
991         }
992         return viewTreeObserver;
993     }
994 
995     @Implementation
onAnimationEnd()996     public void onAnimationEnd() {
997     }
998 
999     /*
1000      * Non-Android accessor.
1001      */
finishedAnimation()1002     public void finishedAnimation() {
1003         try {
1004             Method onAnimationEnd = realView.getClass().getDeclaredMethod("onAnimationEnd", new Class[0]);
1005             onAnimationEnd.setAccessible(true);
1006             onAnimationEnd.invoke(realView);
1007         } catch (Exception e) {
1008             throw new RuntimeException(e);
1009         }
1010     }
1011 }
1012