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