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