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