1package com.xtremelabs.robolectric.shadows;
2
3import android.app.Activity;
4import android.app.Application;
5import android.app.Dialog;
6import android.content.Context;
7import android.content.Intent;
8import android.content.SharedPreferences;
9import android.database.Cursor;
10import android.os.Bundle;
11import android.view.*;
12import android.widget.FrameLayout;
13import com.xtremelabs.robolectric.Robolectric;
14import com.xtremelabs.robolectric.internal.Implementation;
15import com.xtremelabs.robolectric.internal.Implements;
16import com.xtremelabs.robolectric.internal.RealObject;
17import com.xtremelabs.robolectric.tester.android.view.TestWindow;
18
19import java.lang.reflect.InvocationTargetException;
20import java.lang.reflect.Method;
21import java.util.ArrayList;
22import java.util.HashMap;
23import java.util.List;
24import java.util.Map;
25
26import javassist.bytecode.Mnemonic;
27
28import static com.xtremelabs.robolectric.Robolectric.shadowOf;
29
30
31@SuppressWarnings({"UnusedDeclaration"})
32@Implements(Activity.class)
33public 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
62    public final Application getApplication() {
63        return Robolectric.application;
64    }
65
66    @Override
67    @Implementation
68    public final Application getApplicationContext() {
69        return getApplication();
70    }
71
72    @Implementation
73    public void setIntent(Intent intent) {
74        this.intent = intent;
75    }
76
77    @Implementation
78    public Intent getIntent() {
79        return intent;
80    }
81
82    @Implementation(i18nSafe = false)
83    public void setTitle(CharSequence title) {
84        this.title = title;
85    }
86
87    @Implementation
88    public void setTitle(int titleId) {
89        this.title = this.getResources().getString(titleId);
90    }
91
92    @Implementation
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
105    public void setContentView(int layoutResID) {
106        contentView = getLayoutInflater().inflate(layoutResID, new FrameLayout(realActivity));
107        realActivity.onContentChanged();
108    }
109
110    @Implementation
111    public void setContentView(View view) {
112        contentView = view;
113        realActivity.onContentChanged();
114    }
115
116    @Implementation
117    public final void setResult(int resultCode) {
118        this.resultCode = resultCode;
119    }
120
121    @Implementation
122    public final void setResult(int resultCode, Intent data) {
123        this.resultCode = resultCode;
124        this.resultIntent = data;
125    }
126
127    @Implementation
128    public LayoutInflater getLayoutInflater() {
129        return LayoutInflater.from(realActivity);
130    }
131
132    @Implementation
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
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
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
167    public final Activity getParent() {
168        return parent;
169    }
170
171    @Implementation
172    public void onBackPressed() {
173        finish();
174    }
175
176    @Implementation
177    public void finish() {
178        finishWasCalled = true;
179    }
180
181    public void resetIsFinishing() {
182        finishWasCalled = false;
183    }
184
185    /**
186     * @return whether {@link #finish()} was called
187     */
188    @Implementation
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
200    public Window getWindow() {
201        if (window == null) {
202            window = new TestWindow(realActivity);
203        }
204        return window;
205    }
206
207    public void setWindow(TestWindow wind){
208    	window = wind;
209    }
210
211    @Implementation
212    public void runOnUiThread(Runnable action) {
213        Robolectric.getUiThreadScheduler().post(action);
214    }
215
216    @Implementation
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
228    public void onDestroy() {
229        assertNoBroadcastListenersRegistered();
230    }
231
232    @Implementation
233    public WindowManager getWindowManager() {
234        return (WindowManager) Robolectric.application.getSystemService(Context.WINDOW_SERVICE);
235    }
236
237    @Implementation
238    public void setRequestedOrientation(int requestedOrientation) {
239        this.requestedOrientation = requestedOrientation;
240    }
241
242    @Implementation
243    public int getRequestedOrientation() {
244        return requestedOrientation;
245    }
246
247    @Implementation
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     */
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     */
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     */
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     */
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     */
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     */
312    public IntentForResult peekNextStartedActivityForResult() {
313        if (startedActivitiesForResults.isEmpty()) {
314            return null;
315        } else {
316            return startedActivitiesForResults.get(0);
317        }
318    }
319
320    @Implementation
321    public Object getLastNonConfigurationInstance() {
322        return lastNonConfigurationInstance;
323    }
324
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     */
334    public void setCurrentFocus(View view) {
335        currentFocus = view;
336    }
337
338    @Implementation
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
349    public void clearFocus() {
350        currentFocus = null;
351        if (contentView != null) {
352            contentView.clearFocus();
353        }
354    }
355
356    @Implementation
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
366    public boolean onKeyUpWasCalled() {
367        return onKeyUpWasCalled;
368    }
369
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
382        public IntentForResult(Intent intent, int requestCode) {
383            this.intent = intent;
384            this.requestCode = requestCode;
385        }
386    }
387
388    @Implementation
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
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
407    public final void showDialog(int id) {
408        showDialog(id, null);
409    }
410
411    @Implementation
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
422    public final void removeDialog(int id) {
423        dialogForId.remove(id);
424    }
425
426    @Implementation
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     */
459    public Integer getLastShownDialogId() {
460        return lastShownDialogId;
461    }
462
463    public boolean hasCancelledPendingTransitions() {
464        return pendingTransitionEnterAnimResId == 0 && pendingTransitionExitAnimResId == 0;
465    }
466
467    @Implementation
468    public void overridePendingTransition(int enterAnim, int exitAnim) {
469        pendingTransitionEnterAnimResId = enterAnim;
470        pendingTransitionExitAnimResId = exitAnim;
471    }
472
473    public Dialog getDialogById(int dialogId) {
474        return dialogForId.get(dialogId);
475    }
476
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
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
507    public void startManagingCursor(Cursor c) {
508    	managedCusors.add(c);
509    }
510
511    @Implementation
512    public void stopManagingCursor(Cursor c) {
513    	managedCusors.remove(c);
514    }
515
516    public List<Cursor> getManagedCursors() {
517    	return managedCusors;
518    }
519
520    private final class ActivityInvoker {
521        private Method method;
522
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
533        public Object withNothing() {
534            return with();
535        }
536
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