xref: /aosp_15_r20/external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.N;
4 import static android.os.Build.VERSION_CODES.O;
5 import static android.os.Build.VERSION_CODES.Q;
6 import static android.os.Build.VERSION_CODES.R;
7 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
8 import static org.robolectric.util.ReflectionHelpers.getField;
9 import static org.robolectric.util.reflector.Reflector.reflector;
10 
11 import android.annotation.SuppressLint;
12 import android.content.Context;
13 import android.graphics.Canvas;
14 import android.graphics.Paint;
15 import android.graphics.Point;
16 import android.graphics.Rect;
17 import android.graphics.drawable.Drawable;
18 import android.os.Build;
19 import android.os.Looper;
20 import android.os.RemoteException;
21 import android.os.SystemClock;
22 import android.text.TextUtils;
23 import android.util.AttributeSet;
24 import android.view.Choreographer;
25 import android.view.IWindowFocusObserver;
26 import android.view.IWindowId;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewParent;
30 import android.view.WindowId;
31 import android.view.animation.Animation;
32 import android.view.animation.Transformation;
33 import com.google.common.annotations.Beta;
34 import com.google.common.collect.ImmutableList;
35 import java.io.PrintStream;
36 import java.time.Duration;
37 import java.util.ArrayList;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Set;
41 import java.util.concurrent.CopyOnWriteArrayList;
42 import org.robolectric.RuntimeEnvironment;
43 import org.robolectric.annotation.GraphicsMode;
44 import org.robolectric.annotation.GraphicsMode.Mode;
45 import org.robolectric.annotation.Implementation;
46 import org.robolectric.annotation.Implements;
47 import org.robolectric.annotation.LooperMode;
48 import org.robolectric.annotation.RealObject;
49 import org.robolectric.annotation.ReflectorObject;
50 import org.robolectric.annotation.Resetter;
51 import org.robolectric.config.ConfigurationRegistry;
52 import org.robolectric.shadow.api.Shadow;
53 import org.robolectric.shadows.ShadowViewRootImpl.ViewRootImplReflector;
54 import org.robolectric.util.reflector.Accessor;
55 import org.robolectric.util.reflector.Direct;
56 import org.robolectric.util.reflector.ForType;
57 
58 @Implements(View.class)
59 @SuppressLint("NewApi")
60 public class ShadowView {
61 
62   @RealObject protected View realView;
63   @ReflectorObject protected _View_ viewReflector;
64   private static final List<View.OnClickListener> globalClickListeners =
65       new CopyOnWriteArrayList<>();
66   private static final List<View.OnLongClickListener> globalLongClickListeners =
67       new CopyOnWriteArrayList<>();
68   private View.OnClickListener onClickListener;
69   private View.OnLongClickListener onLongClickListener;
70   private View.OnFocusChangeListener onFocusChangeListener;
71   private View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener;
72   private final HashSet<View.OnAttachStateChangeListener> onAttachStateChangeListeners =
73       new HashSet<>();
74   private final HashSet<View.OnLayoutChangeListener> onLayoutChangeListeners = new HashSet<>();
75   private boolean wasInvalidated;
76   private View.OnTouchListener onTouchListener;
77   protected AttributeSet attributeSet;
78   public Point scrollToCoordinates = new Point();
79   private boolean didRequestLayout;
80   private MotionEvent lastTouchEvent;
81   private int hapticFeedbackPerformed = -1;
82   private boolean onLayoutWasCalled;
83   private View.OnCreateContextMenuListener onCreateContextMenuListener;
84   private Rect globalVisibleRect;
85   private int layerType;
86   private final ArrayList<Animation> animations = new ArrayList<>();
87   private AnimationRunner animationRunner;
88 
89   /**
90    * Calls {@code performClick()} on a {@code View} after ensuring that it and its ancestors are
91    * visible and that it is enabled.
92    *
93    * @param view the view to click on
94    * @return true if {@code View.OnClickListener}s were found and fired, false otherwise.
95    * @throws RuntimeException if the preconditions are not met.
96    * @deprecated Please use Espresso for view interactions
97    */
98   @Deprecated
clickOn(View view)99   public static boolean clickOn(View view) {
100     ShadowView shadowView = Shadow.extract(view);
101     return shadowView.checkedPerformClick();
102   }
103 
104   /**
105    * Returns a textual representation of the appearance of the object.
106    *
107    * @param view the view to visualize
108    * @return Textual representation of the appearance of the object.
109    */
visualize(View view)110   public static String visualize(View view) {
111     Canvas canvas = new Canvas();
112     view.draw(canvas);
113     if (!useRealGraphics()) {
114       ShadowCanvas shadowCanvas = Shadow.extract(canvas);
115       return shadowCanvas.getDescription();
116     } else {
117       return "";
118     }
119   }
120 
121   /**
122    * Emits an xml-like representation of the view to System.out.
123    *
124    * @param view the view to dump.
125    * @deprecated - Please use {@link androidx.test.espresso.util.HumanReadables#describe(View)}
126    */
127   @SuppressWarnings("UnusedDeclaration")
128   @Deprecated
dump(View view)129   public static void dump(View view) {
130     ShadowView shadowView = Shadow.extract(view);
131     shadowView.dump();
132   }
133 
134   /**
135    * Returns the text contained within this view.
136    *
137    * @param view the view to scan for text
138    * @return Text contained within this view.
139    */
140   @SuppressWarnings("UnusedDeclaration")
innerText(View view)141   public static String innerText(View view) {
142     ShadowView shadowView = Shadow.extract(view);
143     return shadowView.innerText();
144   }
145 
getLocationInSurfaceCompat(View view)146   static int[] getLocationInSurfaceCompat(View view) {
147     int[] locationInSurface = new int[2];
148     if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
149       view.getLocationInSurface(locationInSurface);
150     } else {
151       view.getLocationInWindow(locationInSurface);
152       Rect surfaceInsets =
153           reflector(ViewRootImplReflector.class, view.getViewRootImpl())
154               .getWindowAttributes()
155               .surfaceInsets;
156       locationInSurface[0] += surfaceInsets.left;
157       locationInSurface[1] += surfaceInsets.top;
158     }
159     return locationInSurface;
160   }
161 
162   /* Note: maxSdk is R because capturing `attributeSet` is not needed any more after R. */
163   @Implementation(maxSdk = R)
__constructor__( Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes)164   protected void __constructor__(
165       Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
166     this.attributeSet = attributeSet;
167     reflector(_View_.class, realView)
168         .__constructor__(context, attributeSet, defStyleAttr, defStyleRes);
169   }
170 
171   @Implementation
setLayerType(int layerType, Paint paint)172   protected void setLayerType(int layerType, Paint paint) {
173     this.layerType = layerType;
174     reflector(_View_.class, realView).setLayerType(layerType, paint);
175   }
176 
177   @Implementation
setOnFocusChangeListener(View.OnFocusChangeListener l)178   protected void setOnFocusChangeListener(View.OnFocusChangeListener l) {
179     onFocusChangeListener = l;
180     reflector(_View_.class, realView).setOnFocusChangeListener(l);
181   }
182 
183   @Implementation
setOnClickListener(View.OnClickListener onClickListener)184   protected void setOnClickListener(View.OnClickListener onClickListener) {
185     this.onClickListener = onClickListener;
186     reflector(_View_.class, realView).setOnClickListener(onClickListener);
187   }
188 
189   @Implementation
setOnLongClickListener(View.OnLongClickListener onLongClickListener)190   protected void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
191     this.onLongClickListener = onLongClickListener;
192     reflector(_View_.class, realView).setOnLongClickListener(onLongClickListener);
193   }
194 
195   @Implementation
setOnSystemUiVisibilityChangeListener( View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener)196   protected void setOnSystemUiVisibilityChangeListener(
197       View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener) {
198     this.onSystemUiVisibilityChangeListener = onSystemUiVisibilityChangeListener;
199     reflector(_View_.class, realView)
200         .setOnSystemUiVisibilityChangeListener(onSystemUiVisibilityChangeListener);
201   }
202 
203   @Implementation
setOnCreateContextMenuListener( View.OnCreateContextMenuListener onCreateContextMenuListener)204   protected void setOnCreateContextMenuListener(
205       View.OnCreateContextMenuListener onCreateContextMenuListener) {
206     this.onCreateContextMenuListener = onCreateContextMenuListener;
207     reflector(_View_.class, realView).setOnCreateContextMenuListener(onCreateContextMenuListener);
208   }
209 
210   @Implementation
addOnAttachStateChangeListener( View.OnAttachStateChangeListener onAttachStateChangeListener)211   protected void addOnAttachStateChangeListener(
212       View.OnAttachStateChangeListener onAttachStateChangeListener) {
213     onAttachStateChangeListeners.add(onAttachStateChangeListener);
214     reflector(_View_.class, realView).addOnAttachStateChangeListener(onAttachStateChangeListener);
215   }
216 
217   @Implementation
removeOnAttachStateChangeListener( View.OnAttachStateChangeListener onAttachStateChangeListener)218   protected void removeOnAttachStateChangeListener(
219       View.OnAttachStateChangeListener onAttachStateChangeListener) {
220     onAttachStateChangeListeners.remove(onAttachStateChangeListener);
221     reflector(_View_.class, realView)
222         .removeOnAttachStateChangeListener(onAttachStateChangeListener);
223   }
224 
225   @Implementation
addOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener)226   protected void addOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener) {
227     onLayoutChangeListeners.add(onLayoutChangeListener);
228     reflector(_View_.class, realView).addOnLayoutChangeListener(onLayoutChangeListener);
229   }
230 
231   @Implementation
removeOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener)232   protected void removeOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener) {
233     onLayoutChangeListeners.remove(onLayoutChangeListener);
234     reflector(_View_.class, realView).removeOnLayoutChangeListener(onLayoutChangeListener);
235   }
236 
237   @Implementation
draw(Canvas canvas)238   protected void draw(Canvas canvas) {
239     Drawable background = realView.getBackground();
240     if (background != null && !useRealGraphics()) {
241       Object shadowCanvas = Shadow.extract(canvas);
242       // Check that Canvas is not a Mockito mock
243       if (shadowCanvas instanceof ShadowCanvas) {
244         ((ShadowCanvas) shadowCanvas).appendDescription("background:");
245       }
246     }
247     reflector(_View_.class, realView).draw(canvas);
248   }
249 
250   @Implementation
onLayout(boolean changed, int left, int top, int right, int bottom)251   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
252     onLayoutWasCalled = true;
253     reflector(_View_.class, realView).onLayout(changed, left, top, right, bottom);
254   }
255 
onLayoutWasCalled()256   public boolean onLayoutWasCalled() {
257     return onLayoutWasCalled;
258   }
259 
260   @Implementation
requestLayout()261   protected void requestLayout() {
262     didRequestLayout = true;
263     reflector(_View_.class, realView).requestLayout();
264   }
265 
266   @Implementation
performClick()267   protected boolean performClick() {
268     for (View.OnClickListener listener : globalClickListeners) {
269       listener.onClick(realView);
270     }
271     return reflector(_View_.class, realView).performClick();
272   }
273 
274   /**
275    * Registers an {@link View.OnClickListener} to the {@link ShadowView}.
276    *
277    * @param listener The {@link View.OnClickListener} to be registered.
278    */
addGlobalPerformClickListener(View.OnClickListener listener)279   public static void addGlobalPerformClickListener(View.OnClickListener listener) {
280     ShadowView.globalClickListeners.add(listener);
281   }
282 
283   /**
284    * Removes an {@link View.OnClickListener} from the {@link ShadowView}.
285    *
286    * @param listener The {@link View.OnClickListener} to be removed.
287    */
removeGlobalPerformClickListener(View.OnClickListener listener)288   public static void removeGlobalPerformClickListener(View.OnClickListener listener) {
289     ShadowView.globalClickListeners.remove(listener);
290   }
291 
292   @Implementation
performLongClick()293   protected boolean performLongClick() {
294     for (View.OnLongClickListener listener : globalLongClickListeners) {
295       listener.onLongClick(realView);
296     }
297     return reflector(_View_.class, realView).performLongClick();
298   }
299 
300   /**
301    * Registers an {@link View.OnLongClickListener} to the {@link ShadowView}.
302    *
303    * @param listener The {@link View.OnLongClickListener} to be registered.
304    */
addGlobalPerformLongClickListener(View.OnLongClickListener listener)305   public static void addGlobalPerformLongClickListener(View.OnLongClickListener listener) {
306     ShadowView.globalLongClickListeners.add(listener);
307   }
308 
309   /**
310    * Removes an {@link View.OnLongClickListener} from the {@link ShadowView}.
311    *
312    * @param listener The {@link View.OnLongClickListener} to be removed.
313    */
removeGlobalPerformLongClickListener(View.OnLongClickListener listener)314   public static void removeGlobalPerformLongClickListener(View.OnLongClickListener listener) {
315     ShadowView.globalLongClickListeners.remove(listener);
316   }
317 
318   @Resetter
reset()319   public static void reset() {
320     ShadowView.globalClickListeners.clear();
321     ShadowView.globalLongClickListeners.clear();
322   }
323 
didRequestLayout()324   public boolean didRequestLayout() {
325     return didRequestLayout;
326   }
327 
setDidRequestLayout(boolean didRequestLayout)328   public void setDidRequestLayout(boolean didRequestLayout) {
329     this.didRequestLayout = didRequestLayout;
330   }
331 
setViewFocus(boolean hasFocus)332   public void setViewFocus(boolean hasFocus) {
333     if (onFocusChangeListener != null) {
334       onFocusChangeListener.onFocusChange(realView, hasFocus);
335     }
336   }
337 
338   @Implementation
invalidate()339   protected void invalidate() {
340     wasInvalidated = true;
341     reflector(_View_.class, realView).invalidate();
342   }
343 
344   @Implementation
onTouchEvent(MotionEvent event)345   protected boolean onTouchEvent(MotionEvent event) {
346     lastTouchEvent = event;
347     return reflector(_View_.class, realView).onTouchEvent(event);
348   }
349 
350   @Implementation
setOnTouchListener(View.OnTouchListener onTouchListener)351   protected void setOnTouchListener(View.OnTouchListener onTouchListener) {
352     this.onTouchListener = onTouchListener;
353     reflector(_View_.class, realView).setOnTouchListener(onTouchListener);
354   }
355 
getLastTouchEvent()356   public MotionEvent getLastTouchEvent() {
357     return lastTouchEvent;
358   }
359 
360   /**
361    * Returns a string representation of this {@code View}. Unless overridden, it will be an empty
362    * string.
363    *
364    * <p>Robolectric extension.
365    *
366    * @return String representation of this view.
367    */
innerText()368   public String innerText() {
369     return "";
370   }
371 
372   /**
373    * Dumps the status of this {@code View} to {@code System.out}
374    *
375    * @deprecated - Please use {@link androidx.test.espresso.util.HumanReadables#describe(View)}
376    */
377   @Deprecated
dump()378   public void dump() {
379     dump(System.out, 0);
380   }
381 
382   /**
383    * Dumps the status of this {@code View} to {@code System.out} at the given indentation level
384    *
385    * @param out Output stream.
386    * @param indent Indentation level.
387    * @deprecated - Please use {@link androidx.test.espresso.util.HumanReadables#describe(View)}
388    */
389   @Deprecated
dump(PrintStream out, int indent)390   public void dump(PrintStream out, int indent) {
391     dumpFirstPart(out, indent);
392     out.println("/>");
393   }
394 
395   @Deprecated
dumpFirstPart(PrintStream out, int indent)396   protected void dumpFirstPart(PrintStream out, int indent) {
397     dumpIndent(out, indent);
398 
399     out.print("<" + realView.getClass().getSimpleName());
400     dumpAttributes(out);
401   }
402 
403   @Deprecated
dumpAttributes(PrintStream out)404   protected void dumpAttributes(PrintStream out) {
405     if (realView.getId() > 0) {
406       dumpAttribute(
407           out, "id", realView.getContext().getResources().getResourceName(realView.getId()));
408     }
409 
410     switch (realView.getVisibility()) {
411       case View.VISIBLE:
412         break;
413       case View.INVISIBLE:
414         dumpAttribute(out, "visibility", "INVISIBLE");
415         break;
416       case View.GONE:
417         dumpAttribute(out, "visibility", "GONE");
418         break;
419     }
420   }
421 
422   @Deprecated
dumpAttribute(PrintStream out, String name, String value)423   protected void dumpAttribute(PrintStream out, String name, String value) {
424     out.print(" " + name + "=\"" + (value == null ? null : TextUtils.htmlEncode(value)) + "\"");
425   }
426 
427   @Deprecated
dumpIndent(PrintStream out, int indent)428   protected void dumpIndent(PrintStream out, int indent) {
429     for (int i = 0; i < indent; i++) out.print(" ");
430   }
431 
432   /**
433    * @return whether or not {@link #invalidate()} has been called
434    */
wasInvalidated()435   public boolean wasInvalidated() {
436     return wasInvalidated;
437   }
438 
439   /** Clears the wasInvalidated flag */
clearWasInvalidated()440   public void clearWasInvalidated() {
441     wasInvalidated = false;
442   }
443 
444   /**
445    * Utility method for clicking on views exposing testing scenarios that are not possible when
446    * using the actual app.
447    *
448    * <p>If running with LooperMode PAUSED will also idle the main Looper.
449    *
450    * @throws RuntimeException if the view is disabled or if the view or any of its parents are not
451    *     visible.
452    * @return Return value of the underlying click operation.
453    * @deprecated - Please use Espresso for View interactions.
454    */
455   @Deprecated
checkedPerformClick()456   public boolean checkedPerformClick() {
457     if (!realView.isShown()) {
458       throw new RuntimeException("View is not visible and cannot be clicked");
459     }
460     if (!realView.isEnabled()) {
461       throw new RuntimeException("View is not enabled and cannot be clicked");
462     }
463     boolean res = realView.performClick();
464     shadowMainLooper().idleIfPaused();
465     return res;
466   }
467 
468   /**
469    * @return Touch listener, if set.
470    */
getOnTouchListener()471   public View.OnTouchListener getOnTouchListener() {
472     return onTouchListener;
473   }
474 
475   /**
476    * @return Returns click listener, if set.
477    */
getOnClickListener()478   public View.OnClickListener getOnClickListener() {
479     return onClickListener;
480   }
481 
482   /**
483    * @return Returns long click listener, if set.
484    */
485   @Implementation(minSdk = R)
getOnLongClickListener()486   public View.OnLongClickListener getOnLongClickListener() {
487     if (RuntimeEnvironment.getApiLevel() >= R) {
488       return reflector(_View_.class, realView).getOnLongClickListener();
489     } else {
490       return onLongClickListener;
491     }
492   }
493 
494   /**
495    * @return Returns system ui visibility change listener.
496    */
getOnSystemUiVisibilityChangeListener()497   public View.OnSystemUiVisibilityChangeListener getOnSystemUiVisibilityChangeListener() {
498     return onSystemUiVisibilityChangeListener;
499   }
500 
501   /**
502    * @return Returns create ContextMenu listener, if set.
503    */
getOnCreateContextMenuListener()504   public View.OnCreateContextMenuListener getOnCreateContextMenuListener() {
505     return onCreateContextMenuListener;
506   }
507 
508   /**
509    * @return Returns the attached listeners, or the empty set if none are present.
510    */
getOnAttachStateChangeListeners()511   public Set<View.OnAttachStateChangeListener> getOnAttachStateChangeListeners() {
512     return onAttachStateChangeListeners;
513   }
514 
515   /**
516    * @return Returns the layout change listeners, or the empty set if none are present.
517    */
getOnLayoutChangeListeners()518   public Set<View.OnLayoutChangeListener> getOnLayoutChangeListeners() {
519     return onLayoutChangeListeners;
520   }
521 
522   @Implementation
post(Runnable action)523   protected boolean post(Runnable action) {
524     if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
525       ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
526       return true;
527     } else {
528       return reflector(_View_.class, realView).post(action);
529     }
530   }
531 
532   @Implementation
postDelayed(Runnable action, long delayMills)533   protected boolean postDelayed(Runnable action, long delayMills) {
534     if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
535       ShadowApplication.getInstance()
536           .getForegroundThreadScheduler()
537           .postDelayed(action, delayMills);
538       return true;
539     } else {
540       return reflector(_View_.class, realView).postDelayed(action, delayMills);
541     }
542   }
543 
544   @Implementation
postInvalidateDelayed(long delayMilliseconds)545   protected void postInvalidateDelayed(long delayMilliseconds) {
546     if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
547       ShadowApplication.getInstance()
548           .getForegroundThreadScheduler()
549           .postDelayed(
550               new Runnable() {
551                 @Override
552                 public void run() {
553                   realView.invalidate();
554                 }
555               },
556               delayMilliseconds);
557     } else {
558       reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
559     }
560   }
561 
562   @Implementation
removeCallbacks(Runnable callback)563   protected boolean removeCallbacks(Runnable callback) {
564     if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
565       ShadowLegacyLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
566       shadowLooper.getScheduler().remove(callback);
567       return true;
568     } else {
569       return reflector(_View_.class, realView).removeCallbacks(callback);
570     }
571   }
572 
573   @Implementation
scrollTo(int x, int y)574   protected void scrollTo(int x, int y) {
575     if (useRealScrolling()) {
576       reflector(_View_.class, realView).scrollTo(x, y);
577     } else {
578       reflector(_View_.class, realView)
579           .onScrollChanged(x, y, scrollToCoordinates.x, scrollToCoordinates.y);
580       scrollToCoordinates = new Point(x, y);
581       reflector(_View_.class, realView).setMemberScrollX(x);
582       reflector(_View_.class, realView).setMemberScrollY(y);
583     }
584   }
585 
586   @Implementation
scrollBy(int x, int y)587   protected void scrollBy(int x, int y) {
588     if (useRealScrolling()) {
589       reflector(_View_.class, realView).scrollBy(x, y);
590     } else {
591       scrollTo(getScrollX() + x, getScrollY() + y);
592     }
593   }
594 
595   @Implementation
getScrollX()596   protected int getScrollX() {
597     if (useRealScrolling()) {
598       return reflector(_View_.class, realView).getScrollX();
599     } else {
600       return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
601     }
602   }
603 
604   @Implementation
getScrollY()605   protected int getScrollY() {
606     if (useRealScrolling()) {
607       return reflector(_View_.class, realView).getScrollY();
608     } else {
609       return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
610     }
611   }
612 
613   @Implementation
setScrollX(int scrollX)614   protected void setScrollX(int scrollX) {
615     if (useRealScrolling()) {
616       reflector(_View_.class, realView).setScrollX(scrollX);
617     } else {
618       scrollTo(scrollX, scrollToCoordinates.y);
619     }
620   }
621 
622   @Implementation
setScrollY(int scrollY)623   protected void setScrollY(int scrollY) {
624     if (useRealScrolling()) {
625       reflector(_View_.class, realView).setScrollY(scrollY);
626     } else {
627       scrollTo(scrollToCoordinates.x, scrollY);
628     }
629   }
630 
631   @Implementation
getLayerType()632   protected int getLayerType() {
633     return this.layerType;
634   }
635 
636   /** Returns a list of all animations that have been set on this view. */
getAnimations()637   public ImmutableList<Animation> getAnimations() {
638     return ImmutableList.copyOf(animations);
639   }
640 
641   /** Resets the list returned by {@link #getAnimations()} to an empty list. */
clearAnimations()642   public void clearAnimations() {
643     animations.clear();
644   }
645 
646   @Implementation
setAnimation(final Animation animation)647   protected void setAnimation(final Animation animation) {
648     reflector(_View_.class, realView).setAnimation(animation);
649 
650     if (animation != null) {
651       animations.add(animation);
652       if (animationRunner != null) {
653         animationRunner.cancel();
654       }
655       animationRunner = new AnimationRunner(animation);
656       animationRunner.start();
657     }
658   }
659 
660   @Implementation
clearAnimation()661   protected void clearAnimation() {
662     reflector(_View_.class, realView).clearAnimation();
663 
664     if (animationRunner != null) {
665       animationRunner.cancel();
666       animationRunner = null;
667     }
668   }
669 
670   @Implementation
initialAwakenScrollBars()671   protected boolean initialAwakenScrollBars() {
672     // Temporarily allow disabling initial awaken of scroll bars to aid in migration of tests to
673     // default to window's being marked visible, this will be removed once migration is complete.
674     if (Boolean.getBoolean("robolectric.disableInitialAwakenScrollBars")) {
675       return false;
676     } else {
677       return viewReflector.initialAwakenScrollBars();
678     }
679   }
680 
681   private class AnimationRunner implements Runnable {
682     private final Animation animation;
683     private final Transformation transformation = new Transformation();
684     private long startTime;
685     private long elapsedTime;
686     private boolean canceled;
687 
AnimationRunner(Animation animation)688     AnimationRunner(Animation animation) {
689       this.animation = animation;
690     }
691 
start()692     private void start() {
693       startTime = animation.getStartTime();
694       long startOffset = animation.getStartOffset();
695       long startDelay =
696           startTime == Animation.START_ON_FIRST_FRAME
697               ? startOffset
698               : (startTime + startOffset) - SystemClock.uptimeMillis();
699       Choreographer.getInstance()
700           .postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, this, null, startDelay);
701     }
702 
step()703     private boolean step() {
704       long animationTime =
705           animation.getStartTime() == Animation.START_ON_FIRST_FRAME
706               ? SystemClock.uptimeMillis()
707               : (animation.getStartTime() + animation.getStartOffset() + elapsedTime);
708       // Note in real android the parent is non-nullable, retain legacy robolectric behavior which
709       // allows detached views to animate.
710       if (!animation.isInitialized() && realView.getParent() != null) {
711         View parent = (View) realView.getParent();
712         animation.initialize(
713             realView.getWidth(), realView.getHeight(), parent.getWidth(), parent.getHeight());
714       }
715       boolean next = animation.getTransformation(animationTime, transformation);
716       // Note in real view implementation it doesn't check the animation equality before clearing,
717       // but in the real implementation the animation listeners are posted so it doesn't race with
718       // chained animations.
719       if (realView.getAnimation() == animation && !next) {
720         if (!animation.getFillAfter()) {
721           realView.clearAnimation();
722         }
723       }
724       // We can't handle infinitely repeating animations in the current scheduling model, so abort
725       // after one iteration.
726       return next
727           && (animation.getRepeatCount() != Animation.INFINITE
728               || elapsedTime < animation.getDuration());
729     }
730 
731     @Override
run()732     public void run() {
733       // Abort if start time has been messed with, as this simulation is only designed to handle
734       // standard situations.
735       if (!canceled && animation.getStartTime() == startTime && step()) {
736         // Start time updates for repeating animations and if START_ON_FIRST_FRAME.
737         startTime = animation.getStartTime();
738         elapsedTime +=
739             ShadowLooper.looperMode().equals(LooperMode.Mode.LEGACY)
740                 ? Duration.ofNanos(ShadowChoreographer.getFrameInterval()).toMillis()
741                 : ShadowChoreographer.getFrameDelay().toMillis();
742         Choreographer.getInstance().postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
743       } else if (animationRunner == this) {
744         animationRunner = null;
745       }
746     }
747 
cancel()748     public void cancel() {
749       this.canceled = true;
750       Choreographer.getInstance()
751           .removeCallbacks(Choreographer.CALLBACK_ANIMATION, animationRunner, null);
752     }
753   }
754 
755   @Implementation
isAttachedToWindow()756   protected boolean isAttachedToWindow() {
757     return getAttachInfo() != null;
758   }
759 
getAttachInfo()760   private Object getAttachInfo() {
761     return reflector(_View_.class, realView).getAttachInfo();
762   }
763 
764   /** Reflector interface for {@link View}'s internals. */
765   @ForType(View.class)
766   private interface _View_ {
767 
768     @Direct
draw(Canvas canvas)769     void draw(Canvas canvas);
770 
771     @Direct
onLayout(boolean changed, int left, int top, int right, int bottom)772     void onLayout(boolean changed, int left, int top, int right, int bottom);
773 
assignParent(ViewParent viewParent)774     void assignParent(ViewParent viewParent);
775 
776     @Direct
setOnFocusChangeListener(View.OnFocusChangeListener l)777     void setOnFocusChangeListener(View.OnFocusChangeListener l);
778 
779     @Direct
setLayerType(int layerType, Paint paint)780     void setLayerType(int layerType, Paint paint);
781 
782     @Direct
setOnClickListener(View.OnClickListener onClickListener)783     void setOnClickListener(View.OnClickListener onClickListener);
784 
785     @Direct
setOnLongClickListener(View.OnLongClickListener onLongClickListener)786     void setOnLongClickListener(View.OnLongClickListener onLongClickListener);
787 
788     @Direct
getOnLongClickListener()789     View.OnLongClickListener getOnLongClickListener();
790 
791     @Direct
setOnSystemUiVisibilityChangeListener( View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener)792     void setOnSystemUiVisibilityChangeListener(
793         View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener);
794 
795     @Direct
setOnCreateContextMenuListener( View.OnCreateContextMenuListener onCreateContextMenuListener)796     void setOnCreateContextMenuListener(
797         View.OnCreateContextMenuListener onCreateContextMenuListener);
798 
799     @Direct
addOnAttachStateChangeListener( View.OnAttachStateChangeListener onAttachStateChangeListener)800     void addOnAttachStateChangeListener(
801         View.OnAttachStateChangeListener onAttachStateChangeListener);
802 
803     @Direct
removeOnAttachStateChangeListener( View.OnAttachStateChangeListener onAttachStateChangeListener)804     void removeOnAttachStateChangeListener(
805         View.OnAttachStateChangeListener onAttachStateChangeListener);
806 
807     @Direct
addOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener)808     void addOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener);
809 
810     @Direct
removeOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener)811     void removeOnLayoutChangeListener(View.OnLayoutChangeListener onLayoutChangeListener);
812 
813     @Direct
requestLayout()814     void requestLayout();
815 
816     @Direct
performClick()817     boolean performClick();
818 
819     @Direct
performLongClick()820     boolean performLongClick();
821 
822     @Direct
invalidate()823     void invalidate();
824 
825     @Direct
onTouchEvent(MotionEvent event)826     boolean onTouchEvent(MotionEvent event);
827 
828     @Direct
setOnTouchListener(View.OnTouchListener onTouchListener)829     void setOnTouchListener(View.OnTouchListener onTouchListener);
830 
831     @Direct
post(Runnable action)832     boolean post(Runnable action);
833 
834     @Direct
postDelayed(Runnable action, long delayMills)835     boolean postDelayed(Runnable action, long delayMills);
836 
837     @Direct
postInvalidateDelayed(long delayMilliseconds)838     void postInvalidateDelayed(long delayMilliseconds);
839 
840     @Direct
removeCallbacks(Runnable callback)841     boolean removeCallbacks(Runnable callback);
842 
843     @Direct
setAnimation(final Animation animation)844     void setAnimation(final Animation animation);
845 
846     @Direct
clearAnimation()847     void clearAnimation();
848 
849     @Direct
getGlobalVisibleRect(Rect rect, Point globalOffset)850     boolean getGlobalVisibleRect(Rect rect, Point globalOffset);
851 
852     @Direct
getWindowId()853     WindowId getWindowId();
854 
855     @Accessor("mAttachInfo")
getAttachInfo()856     Object getAttachInfo();
857 
onAttachedToWindow()858     void onAttachedToWindow();
859 
onDetachedFromWindow()860     void onDetachedFromWindow();
861 
onScrollChanged(int l, int t, int oldl, int oldt)862     void onScrollChanged(int l, int t, int oldl, int oldt);
863 
864     @Direct
getSourceLayoutResId()865     int getSourceLayoutResId();
866 
867     @Direct
initialAwakenScrollBars()868     boolean initialAwakenScrollBars();
869 
870     @Accessor("mScrollX")
setMemberScrollX(int value)871     void setMemberScrollX(int value);
872 
873     @Accessor("mScrollY")
setMemberScrollY(int value)874     void setMemberScrollY(int value);
875 
876     @Direct
scrollTo(int x, int y)877     void scrollTo(int x, int y);
878 
879     @Direct
scrollBy(int x, int y)880     void scrollBy(int x, int y);
881 
882     @Direct
getScrollX()883     int getScrollX();
884 
885     @Direct
getScrollY()886     int getScrollY();
887 
888     @Direct
setScrollX(int value)889     void setScrollX(int value);
890 
891     @Direct
setScrollY(int value)892     void setScrollY(int value);
893 
894     @Direct
__constructor__(Context context, AttributeSet attributeSet, int defStyle)895     void __constructor__(Context context, AttributeSet attributeSet, int defStyle);
896 
897     @Direct
__constructor__( Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes)898     void __constructor__(
899         Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes);
900   }
901 
callOnAttachedToWindow()902   public void callOnAttachedToWindow() {
903     reflector(_View_.class, realView).onAttachedToWindow();
904   }
905 
callOnDetachedFromWindow()906   public void callOnDetachedFromWindow() {
907     reflector(_View_.class, realView).onDetachedFromWindow();
908   }
909 
910   @Implementation
getWindowId()911   protected WindowId getWindowId() {
912     return WindowIdHelper.getWindowId(this);
913   }
914 
915   @Implementation
performHapticFeedback(int hapticFeedbackType)916   protected boolean performHapticFeedback(int hapticFeedbackType) {
917     hapticFeedbackPerformed = hapticFeedbackType;
918     return true;
919   }
920 
921   @Implementation
getGlobalVisibleRect(Rect rect, Point globalOffset)922   protected boolean getGlobalVisibleRect(Rect rect, Point globalOffset) {
923     if (globalVisibleRect == null) {
924       return reflector(_View_.class, realView).getGlobalVisibleRect(rect, globalOffset);
925     }
926 
927     if (!globalVisibleRect.isEmpty()) {
928       rect.set(globalVisibleRect);
929       if (globalOffset != null) {
930         rect.offset(-globalOffset.x, -globalOffset.y);
931       }
932       return true;
933     }
934     rect.setEmpty();
935     return false;
936   }
937 
setGlobalVisibleRect(Rect rect)938   public void setGlobalVisibleRect(Rect rect) {
939     if (rect != null) {
940       globalVisibleRect = new Rect();
941       globalVisibleRect.set(rect);
942     } else {
943       globalVisibleRect = null;
944     }
945   }
946 
lastHapticFeedbackPerformed()947   public int lastHapticFeedbackPerformed() {
948     return hapticFeedbackPerformed;
949   }
950 
setMyParent(ViewParent viewParent)951   public void setMyParent(ViewParent viewParent) {
952     reflector(_View_.class, realView).assignParent(viewParent);
953   }
954 
955   @Implementation
getWindowVisibleDisplayFrame(Rect outRect)956   protected void getWindowVisibleDisplayFrame(Rect outRect) {
957     // TODO: figure out how to simulate this logic instead
958     // if (mAttachInfo != null) {
959     //   mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);
960 
961     ShadowDisplay.getDefaultDisplay().getRectSize(outRect);
962   }
963 
964   @Implementation(minSdk = N)
getWindowDisplayFrame(Rect outRect)965   protected void getWindowDisplayFrame(Rect outRect) {
966     // TODO: figure out how to simulate this logic instead
967     // if (mAttachInfo != null) {
968     //   mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);
969 
970     ShadowDisplay.getDefaultDisplay().getRectSize(outRect);
971   }
972 
973   /**
974    * Returns the layout resource id this view was inflated from. Backwards compatible version of
975    * {@link View#getSourceLayoutResId()}, passes through to the underlying implementation on API
976    * levels where it is supported.
977    */
978   @Implementation(minSdk = Q)
getSourceLayoutResId()979   public int getSourceLayoutResId() {
980     if (RuntimeEnvironment.getApiLevel() >= Q) {
981       return reflector(_View_.class, realView).getSourceLayoutResId();
982     } else {
983       return ShadowResources.getAttributeSetSourceResId(attributeSet);
984     }
985   }
986 
987   public static class WindowIdHelper {
getWindowId(ShadowView shadowView)988     public static WindowId getWindowId(ShadowView shadowView) {
989       if (shadowView.isAttachedToWindow()) {
990         Object attachInfo = shadowView.getAttachInfo();
991         if (getField(attachInfo, "mWindowId") == null) {
992           IWindowId iWindowId = new MyIWindowIdStub();
993           reflector(_AttachInfo_.class, attachInfo).setWindowId(new WindowId(iWindowId));
994           reflector(_AttachInfo_.class, attachInfo).setIWindowId(iWindowId);
995         }
996       }
997 
998       return reflector(_View_.class, shadowView.realView).getWindowId();
999     }
1000 
1001     private static class MyIWindowIdStub extends IWindowId.Stub {
1002       @Override
registerFocusObserver(IWindowFocusObserver iWindowFocusObserver)1003       public void registerFocusObserver(IWindowFocusObserver iWindowFocusObserver)
1004           throws RemoteException {}
1005 
1006       @Override
unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver)1007       public void unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver)
1008           throws RemoteException {}
1009 
1010       @Override
isFocused()1011       public boolean isFocused() throws RemoteException {
1012         return true;
1013       }
1014     }
1015   }
1016 
1017   /** Reflector interface for android.view.View.AttachInfo's internals. */
1018   @ForType(className = "android.view.View$AttachInfo")
1019   interface _AttachInfo_ {
1020 
1021     @Accessor("mIWindowId")
setIWindowId(IWindowId iWindowId)1022     void setIWindowId(IWindowId iWindowId);
1023 
1024     @Accessor("mWindowId")
setWindowId(WindowId windowId)1025     void setWindowId(WindowId windowId);
1026   }
1027 
1028   /**
1029    * Internal API to determine if native graphics is enabled.
1030    *
1031    * <p>This is currently public because it has to be accessed from multiple packages, but it is not
1032    * recommended to depend on this API.
1033    */
1034   @Beta
useRealGraphics()1035   public static boolean useRealGraphics() {
1036     GraphicsMode.Mode graphicsMode = ConfigurationRegistry.get(GraphicsMode.Mode.class);
1037     return graphicsMode == Mode.NATIVE && RuntimeEnvironment.getApiLevel() >= O;
1038   }
1039 
1040   /**
1041    * Currently the default View scrolling implementation is broken and low-fidelity. For instance,
1042    * even if a View has no children, Robolectric will still happily set the scroll position of a
1043    * View. Long-term we want to eliminate this broken behavior, but in the mean time the real
1044    * scrolling behavior is enabled when native graphics are enabled, or when a system property is
1045    * set.
1046    */
useRealScrolling()1047   static boolean useRealScrolling() {
1048     return useRealGraphics()
1049         || Boolean.parseBoolean(System.getProperty("robolectric.useRealScrolling", "true"));
1050   }
1051 }
1052