1 package com.xtremelabs.robolectric.shadows; 2 3 import android.app.Activity; 4 import android.app.Application; 5 import android.app.Dialog; 6 import android.content.Context; 7 import android.content.Intent; 8 import android.content.SharedPreferences; 9 import android.database.Cursor; 10 import android.os.Bundle; 11 import android.view.*; 12 import android.widget.FrameLayout; 13 import com.xtremelabs.robolectric.Robolectric; 14 import com.xtremelabs.robolectric.internal.Implementation; 15 import com.xtremelabs.robolectric.internal.Implements; 16 import com.xtremelabs.robolectric.internal.RealObject; 17 import com.xtremelabs.robolectric.tester.android.view.TestWindow; 18 19 import java.lang.reflect.InvocationTargetException; 20 import java.lang.reflect.Method; 21 import java.util.ArrayList; 22 import java.util.HashMap; 23 import java.util.List; 24 import java.util.Map; 25 26 import javassist.bytecode.Mnemonic; 27 28 import static com.xtremelabs.robolectric.Robolectric.shadowOf; 29 30 31 @SuppressWarnings({"UnusedDeclaration"}) 32 @Implements(Activity.class) 33 public class ShadowActivity extends ShadowContextWrapper { 34 @RealObject 35 protected Activity realActivity; 36 37 private Intent intent; 38 private FrameLayout contentViewContainer; 39 private View contentView; 40 private int orientation; 41 private int resultCode; 42 private Intent resultIntent; 43 private Activity parent; 44 private boolean finishWasCalled; 45 private TestWindow window; 46 47 private List<IntentForResult> startedActivitiesForResults = new ArrayList<IntentForResult>(); 48 49 private Map<Intent, Integer> intentRequestCodeMap = new HashMap<Intent, Integer>(); 50 private int requestedOrientation = -1; 51 private View currentFocus; 52 private Integer lastShownDialogId = null; 53 private int pendingTransitionEnterAnimResId = -1; 54 private int pendingTransitionExitAnimResId = -1; 55 private Object lastNonConfigurationInstance; 56 private Map<Integer, Dialog> dialogForId = new HashMap<Integer, Dialog>(); 57 private CharSequence title; 58 private boolean onKeyUpWasCalled; 59 private ArrayList<Cursor> managedCusors = new ArrayList<Cursor>(); 60 61 @Implementation getApplication()62 public final Application getApplication() { 63 return Robolectric.application; 64 } 65 66 @Override 67 @Implementation getApplicationContext()68 public final Application getApplicationContext() { 69 return getApplication(); 70 } 71 72 @Implementation setIntent(Intent intent)73 public void setIntent(Intent intent) { 74 this.intent = intent; 75 } 76 77 @Implementation getIntent()78 public Intent getIntent() { 79 return intent; 80 } 81 82 @Implementation(i18nSafe = false) setTitle(CharSequence title)83 public void setTitle(CharSequence title) { 84 this.title = title; 85 } 86 87 @Implementation setTitle(int titleId)88 public void setTitle(int titleId) { 89 this.title = this.getResources().getString(titleId); 90 } 91 92 @Implementation getTitle()93 public CharSequence getTitle() { 94 return title; 95 } 96 97 /** 98 * Sets the {@code contentView} for this {@code Activity} by invoking the 99 * {@link android.view.LayoutInflater} 100 * 101 * @param layoutResID ID of the layout to inflate 102 * @see #getContentView() 103 */ 104 @Implementation setContentView(int layoutResID)105 public void setContentView(int layoutResID) { 106 contentView = getLayoutInflater().inflate(layoutResID, new FrameLayout(realActivity)); 107 realActivity.onContentChanged(); 108 } 109 110 @Implementation setContentView(View view)111 public void setContentView(View view) { 112 contentView = view; 113 realActivity.onContentChanged(); 114 } 115 116 @Implementation setResult(int resultCode)117 public final void setResult(int resultCode) { 118 this.resultCode = resultCode; 119 } 120 121 @Implementation setResult(int resultCode, Intent data)122 public final void setResult(int resultCode, Intent data) { 123 this.resultCode = resultCode; 124 this.resultIntent = data; 125 } 126 127 @Implementation getLayoutInflater()128 public LayoutInflater getLayoutInflater() { 129 return LayoutInflater.from(realActivity); 130 } 131 132 @Implementation getMenuInflater()133 public MenuInflater getMenuInflater() { 134 return new MenuInflater(realActivity); 135 } 136 137 /** 138 * Checks to ensure that the{@code contentView} has been set 139 * 140 * @param id ID of the view to find 141 * @return the view 142 * @throws RuntimeException if the {@code contentView} has not been called first 143 */ 144 @Implementation findViewById(int id)145 public View findViewById(int id) { 146 if (id == android.R.id.content) { 147 return getContentViewContainer(); 148 } 149 if (contentView != null) { 150 return contentView.findViewById(id); 151 } else { 152 System.out.println("WARNING: you probably should have called setContentView() first"); 153 Thread.dumpStack(); 154 return null; 155 } 156 } 157 getContentViewContainer()158 private View getContentViewContainer() { 159 if (contentViewContainer == null) { 160 contentViewContainer = new FrameLayout(realActivity); 161 } 162 contentViewContainer.addView(contentView, 0); 163 return contentViewContainer; 164 } 165 166 @Implementation getParent()167 public final Activity getParent() { 168 return parent; 169 } 170 171 @Implementation onBackPressed()172 public void onBackPressed() { 173 finish(); 174 } 175 176 @Implementation finish()177 public void finish() { 178 finishWasCalled = true; 179 } 180 resetIsFinishing()181 public void resetIsFinishing() { 182 finishWasCalled = false; 183 } 184 185 /** 186 * @return whether {@link #finish()} was called 187 */ 188 @Implementation isFinishing()189 public boolean isFinishing() { 190 return finishWasCalled; 191 } 192 193 /** 194 * Constructs a new Window (a {@link com.xtremelabs.robolectric.tester.android.view.TestWindow}) if no window has previously been 195 * set. 196 * 197 * @return the window associated with this Activity 198 */ 199 @Implementation getWindow()200 public Window getWindow() { 201 if (window == null) { 202 window = new TestWindow(realActivity); 203 } 204 return window; 205 } 206 setWindow(TestWindow wind)207 public void setWindow(TestWindow wind){ 208 window = wind; 209 } 210 211 @Implementation runOnUiThread(Runnable action)212 public void runOnUiThread(Runnable action) { 213 Robolectric.getUiThreadScheduler().post(action); 214 } 215 216 @Implementation onCreate(Bundle bundle)217 public void onCreate(Bundle bundle) { 218 219 } 220 221 /** 222 * Checks to see if {@code BroadcastListener}s are still registered. 223 * 224 * @throws RuntimeException if any listeners are still registered 225 * @see #assertNoBroadcastListenersRegistered() 226 */ 227 @Implementation onDestroy()228 public void onDestroy() { 229 assertNoBroadcastListenersRegistered(); 230 } 231 232 @Implementation getWindowManager()233 public WindowManager getWindowManager() { 234 return (WindowManager) Robolectric.application.getSystemService(Context.WINDOW_SERVICE); 235 } 236 237 @Implementation setRequestedOrientation(int requestedOrientation)238 public void setRequestedOrientation(int requestedOrientation) { 239 this.requestedOrientation = requestedOrientation; 240 } 241 242 @Implementation getRequestedOrientation()243 public int getRequestedOrientation() { 244 return requestedOrientation; 245 } 246 247 @Implementation getPreferences(int mode)248 public SharedPreferences getPreferences(int mode) { 249 return ShadowPreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 250 } 251 252 /** 253 * Checks the {@code ApplicationContext} to see if {@code BroadcastListener}s are still registered. 254 * 255 * @throws RuntimeException if any listeners are still registered 256 * @see ShadowApplication#assertNoBroadcastListenersRegistered(android.content.Context, String) 257 */ assertNoBroadcastListenersRegistered()258 public void assertNoBroadcastListenersRegistered() { 259 shadowOf(getApplicationContext()).assertNoBroadcastListenersRegistered(realActivity, "Activity"); 260 } 261 262 /** 263 * Non-Android accessor. 264 * 265 * @return the {@code contentView} set by one of the {@code setContentView()} methods 266 */ getContentView()267 public View getContentView() { 268 return contentView; 269 } 270 271 /** 272 * Non-Android accessor. 273 * 274 * @return the {@code resultCode} set by one of the {@code setResult()} methods 275 */ getResultCode()276 public int getResultCode() { 277 return resultCode; 278 } 279 280 /** 281 * Non-Android accessor. 282 * 283 * @return the {@code Intent} set by {@link #setResult(int, android.content.Intent)} 284 */ getResultIntent()285 public Intent getResultIntent() { 286 return resultIntent; 287 } 288 289 /** 290 * Non-Android accessor consumes and returns the next {@code Intent} on the 291 * started activities for results stack. 292 * 293 * @return the next started {@code Intent} for an activity, wrapped in 294 * an {@link ShadowActivity.IntentForResult} object 295 */ getNextStartedActivityForResult()296 public IntentForResult getNextStartedActivityForResult() { 297 if (startedActivitiesForResults.isEmpty()) { 298 return null; 299 } else { 300 return startedActivitiesForResults.remove(0); 301 } 302 } 303 304 /** 305 * Non-Android accessor returns the most recent {@code Intent} started by 306 * {@link #startActivityForResult(android.content.Intent, int)} without 307 * consuming it. 308 * 309 * @return the most recently started {@code Intent}, wrapped in 310 * an {@link ShadowActivity.IntentForResult} object 311 */ peekNextStartedActivityForResult()312 public IntentForResult peekNextStartedActivityForResult() { 313 if (startedActivitiesForResults.isEmpty()) { 314 return null; 315 } else { 316 return startedActivitiesForResults.get(0); 317 } 318 } 319 320 @Implementation getLastNonConfigurationInstance()321 public Object getLastNonConfigurationInstance() { 322 return lastNonConfigurationInstance; 323 } 324 setLastNonConfigurationInstance(Object lastNonConfigurationInstance)325 public void setLastNonConfigurationInstance(Object lastNonConfigurationInstance) { 326 this.lastNonConfigurationInstance = lastNonConfigurationInstance; 327 } 328 329 /** 330 * Non-Android accessor Sets the {@code View} for this {@code Activity} 331 * 332 * @param view 333 */ setCurrentFocus(View view)334 public void setCurrentFocus(View view) { 335 currentFocus = view; 336 } 337 338 @Implementation getCurrentFocus()339 public View getCurrentFocus() { 340 if (currentFocus != null) { 341 return currentFocus; 342 } else if (contentView != null) { 343 return contentView.findFocus(); 344 } else { 345 return null; 346 } 347 } 348 clearFocus()349 public void clearFocus() { 350 currentFocus = null; 351 if (contentView != null) { 352 contentView.clearFocus(); 353 } 354 } 355 356 @Implementation onKeyUp(int keyCode, KeyEvent event)357 public boolean onKeyUp(int keyCode, KeyEvent event) { 358 onKeyUpWasCalled = true; 359 if (keyCode == KeyEvent.KEYCODE_BACK) { 360 onBackPressed(); 361 return true; 362 } 363 return false; 364 } 365 onKeyUpWasCalled()366 public boolean onKeyUpWasCalled() { 367 return onKeyUpWasCalled; 368 } 369 resetKeyUpWasCalled()370 public void resetKeyUpWasCalled() { 371 onKeyUpWasCalled = false; 372 } 373 374 /** 375 * Container object to hold an Intent, together with the requestCode used 376 * in a call to {@code Activity#startActivityForResult(Intent, int)} 377 */ 378 public class IntentForResult { 379 public Intent intent; 380 public int requestCode; 381 IntentForResult(Intent intent, int requestCode)382 public IntentForResult(Intent intent, int requestCode) { 383 this.intent = intent; 384 this.requestCode = requestCode; 385 } 386 } 387 388 @Implementation startActivityForResult(Intent intent, int requestCode)389 public void startActivityForResult(Intent intent, int requestCode) { 390 intentRequestCodeMap.put(intent, requestCode); 391 startedActivitiesForResults.add(new IntentForResult(intent, requestCode)); 392 getApplicationContext().startActivity(intent); 393 } 394 receiveResult(Intent requestIntent, int resultCode, Intent resultIntent)395 public void receiveResult(Intent requestIntent, int resultCode, Intent resultIntent) { 396 Integer requestCode = intentRequestCodeMap.get(requestIntent); 397 if (requestCode == null) { 398 throw new RuntimeException("No intent matches " + requestIntent + " among " + intentRequestCodeMap.keySet()); 399 } 400 401 final ActivityInvoker invoker = new ActivityInvoker(); 402 invoker.call("onActivityResult", Integer.TYPE, Integer.TYPE, Intent.class) 403 .with(requestCode, resultCode, resultIntent); 404 } 405 406 @Implementation showDialog(int id)407 public final void showDialog(int id) { 408 showDialog(id, null); 409 } 410 411 @Implementation dismissDialog(int id)412 public final void dismissDialog(int id) { 413 final Dialog dialog = dialogForId.get(id); 414 if (dialog == null) { 415 throw new IllegalArgumentException(); 416 } 417 418 dialog.dismiss(); 419 } 420 421 @Implementation removeDialog(int id)422 public final void removeDialog(int id) { 423 dialogForId.remove(id); 424 } 425 426 @Implementation showDialog(int id, Bundle bundle)427 public final boolean showDialog(int id, Bundle bundle) { 428 Dialog dialog = null; 429 this.lastShownDialogId = id; 430 431 dialog = dialogForId.get(id); 432 433 if (dialog == null) { 434 final ActivityInvoker invoker = new ActivityInvoker(); 435 dialog = (Dialog) invoker.call("onCreateDialog", Integer.TYPE).with(id); 436 437 if (bundle == null) { 438 invoker.call("onPrepareDialog", Integer.TYPE, Dialog.class) 439 .with(id, dialog); 440 } else { 441 invoker.call("onPrepareDialog", Integer.TYPE, Dialog.class, Bundle.class) 442 .with(id, dialog, bundle); 443 } 444 445 dialogForId.put(id, dialog); 446 } 447 448 dialog.show(); 449 450 return true; 451 } 452 453 /** 454 * Non-Android accessor 455 * 456 * @return the dialog resource id passed into 457 * {@code Activity#showDialog(int, Bundle)} or {@code Activity#showDialog(int)} 458 */ getLastShownDialogId()459 public Integer getLastShownDialogId() { 460 return lastShownDialogId; 461 } 462 hasCancelledPendingTransitions()463 public boolean hasCancelledPendingTransitions() { 464 return pendingTransitionEnterAnimResId == 0 && pendingTransitionExitAnimResId == 0; 465 } 466 467 @Implementation overridePendingTransition(int enterAnim, int exitAnim)468 public void overridePendingTransition(int enterAnim, int exitAnim) { 469 pendingTransitionEnterAnimResId = enterAnim; 470 pendingTransitionExitAnimResId = exitAnim; 471 } 472 getDialogById(int dialogId)473 public Dialog getDialogById(int dialogId) { 474 return dialogForId.get(dialogId); 475 } 476 create()477 public void create() { 478 final ActivityInvoker invoker = new ActivityInvoker(); 479 480 final Bundle noInstanceState = null; 481 invoker.call("onCreate", Bundle.class).with(noInstanceState); 482 invoker.call("onStart").withNothing(); 483 invoker.call("onPostCreate", Bundle.class).with(noInstanceState); 484 invoker.call("onResume").withNothing(); 485 } 486 487 @Implementation recreate()488 public void recreate() { 489 Bundle outState = new Bundle(); 490 final ActivityInvoker invoker = new ActivityInvoker(); 491 492 invoker.call("onSaveInstanceState", Bundle.class).with(outState); 493 invoker.call("onPause").withNothing(); 494 invoker.call("onStop").withNothing(); 495 496 Object nonConfigInstance = invoker.call("onRetainNonConfigurationInstance").withNothing(); 497 setLastNonConfigurationInstance(nonConfigInstance); 498 499 invoker.call("onDestroy").withNothing(); 500 invoker.call("onCreate", Bundle.class).with(outState); 501 invoker.call("onStart").withNothing(); 502 invoker.call("onRestoreInstanceState", Bundle.class).with(outState); 503 invoker.call("onResume").withNothing(); 504 } 505 506 @Implementation startManagingCursor(Cursor c)507 public void startManagingCursor(Cursor c) { 508 managedCusors.add(c); 509 } 510 511 @Implementation stopManagingCursor(Cursor c)512 public void stopManagingCursor(Cursor c) { 513 managedCusors.remove(c); 514 } 515 getManagedCursors()516 public List<Cursor> getManagedCursors() { 517 return managedCusors; 518 } 519 520 private final class ActivityInvoker { 521 private Method method; 522 call(final String methodName, final Class ...argumentClasses)523 public ActivityInvoker call(final String methodName, final Class ...argumentClasses) { 524 try { 525 method = Activity.class.getDeclaredMethod(methodName, argumentClasses); 526 method.setAccessible(true); 527 return this; 528 } catch(NoSuchMethodException e) { 529 throw new RuntimeException(e); 530 } 531 } 532 withNothing()533 public Object withNothing() { 534 return with(); 535 } 536 with(final Object ...parameters)537 public Object with(final Object ...parameters) { 538 try { 539 return method.invoke(realActivity, parameters); 540 } catch(IllegalAccessException e) { 541 throw new RuntimeException(e); 542 } catch(IllegalArgumentException e) { 543 throw new RuntimeException(e); 544 } catch(InvocationTargetException e) { 545 throw new RuntimeException(e); 546 } 547 } 548 } 549 } 550