ShadowActivity.java revision 6c243b00fee6298c384d10f2cf97508d24237633
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.os.Bundle;
9import android.view.KeyEvent;
10import android.view.LayoutInflater;
11import android.view.MenuInflater;
12import android.view.View;
13import android.view.Window;
14import android.view.WindowManager;
15import android.widget.FrameLayout;
16import com.xtremelabs.robolectric.Robolectric;
17import com.xtremelabs.robolectric.internal.Implementation;
18import com.xtremelabs.robolectric.internal.Implements;
19import com.xtremelabs.robolectric.internal.RealObject;
20import com.xtremelabs.robolectric.tester.android.view.TestWindow;
21
22import java.lang.reflect.InvocationTargetException;
23import java.lang.reflect.Method;
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.List;
27import java.util.Map;
28
29import static com.xtremelabs.robolectric.Robolectric.shadowOf;
30
31
32@SuppressWarnings({"UnusedDeclaration"})
33@Implements(Activity.class)
34public class ShadowActivity extends ShadowContextWrapper {
35    @RealObject
36    private Activity realActivity;
37
38    private Intent intent;
39    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
60    @Implementation
61    public final Application getApplication() {
62        return Robolectric.application;
63    }
64
65    @Override
66    @Implementation
67    public final Application getApplicationContext() {
68        return getApplication();
69    }
70
71    @Implementation
72    public void setIntent(Intent intent) {
73        this.intent = intent;
74    }
75
76    @Implementation
77    public Intent getIntent() {
78        return intent;
79    }
80
81    @Implementation(i18nSafe = false)
82    public void setTitle(CharSequence title) {
83        this.title = title;
84    }
85
86    @Implementation
87    public void setTitle(int titleId) {
88        this.title = this.getResources().getString(titleId);
89    }
90
91    @Implementation
92    public CharSequence getTitle() {
93        return title;
94    }
95
96    /**
97     * Sets the {@code contentView} for this {@code Activity} by invoking the
98     * {@link android.view.LayoutInflater}
99     *
100     * @param layoutResID ID of the layout to inflate
101     * @see #getContentView()
102     */
103    @Implementation
104    public void setContentView(int layoutResID) {
105        contentView = getLayoutInflater().inflate(layoutResID, new FrameLayout(realActivity));
106        realActivity.onContentChanged();
107    }
108
109    @Implementation
110    public void setContentView(View view) {
111        contentView = view;
112        realActivity.onContentChanged();
113    }
114
115    @Implementation
116    public final void setResult(int resultCode) {
117        this.resultCode = resultCode;
118    }
119
120    @Implementation
121    public final void setResult(int resultCode, Intent data) {
122        this.resultCode = resultCode;
123        this.resultIntent = data;
124    }
125
126    @Implementation
127    public LayoutInflater getLayoutInflater() {
128        return LayoutInflater.from(realActivity);
129    }
130
131    @Implementation
132    public MenuInflater getMenuInflater() {
133        return new MenuInflater(realActivity);
134    }
135
136    /**
137     * Checks to ensure that the{@code contentView} has been set
138     *
139     * @param id ID of the view to find
140     * @return the view
141     * @throws RuntimeException if the {@code contentView} has not been called first
142     */
143    @Implementation
144    public View findViewById(int id) {
145        if (contentView != null) {
146            return contentView.findViewById(id);
147        } else {
148            System.out.println("WARNING: you probably should have called setContentView() first");
149            Thread.dumpStack();
150            return null;
151        }
152    }
153
154    @Implementation
155    public final Activity getParent() {
156        return parent;
157    }
158
159    @Implementation
160    public void onBackPressed() {
161        finish();
162    }
163
164    @Implementation
165    public void finish() {
166        finishWasCalled = true;
167    }
168
169    /**
170     * @return whether {@link #finish()} was called
171     */
172    @Implementation
173    public boolean isFinishing() {
174        return finishWasCalled;
175    }
176
177    /**
178     * Constructs a new Window (a {@link com.xtremelabs.robolectric.tester.android.view.TestWindow}) if no window has previously been
179     * set.
180     *
181     * @return the window associated with this Activity
182     */
183    @Implementation
184    public Window getWindow() {
185        if (window == null) {
186            window = new TestWindow(realActivity);
187        }
188        return window;
189    }
190
191    @Implementation
192    public void runOnUiThread(Runnable action) {
193        Robolectric.getUiThreadScheduler().post(action);
194    }
195
196    /**
197     * Checks to see if {@code BroadcastListener}s are still registered.
198     *
199     * @throws RuntimeException if any listeners are still registered
200     * @see #assertNoBroadcastListenersRegistered()
201     */
202    @Implementation
203    public void onDestroy() {
204        assertNoBroadcastListenersRegistered();
205    }
206
207    @Implementation
208    public WindowManager getWindowManager() {
209        return (WindowManager) Robolectric.application.getSystemService(Context.WINDOW_SERVICE);
210    }
211
212    @Implementation
213    public void setRequestedOrientation(int requestedOrientation) {
214        this.requestedOrientation = requestedOrientation;
215    }
216
217    @Implementation
218    public int getRequestedOrientation() {
219        return requestedOrientation;
220    }
221
222    /**
223     * Checks the {@code ApplicationContext} to see if {@code BroadcastListener}s are still registered.
224     *
225     * @throws RuntimeException if any listeners are still registered
226     * @see ShadowApplication#assertNoBroadcastListenersRegistered(android.content.Context, String)
227     */
228    public void assertNoBroadcastListenersRegistered() {
229        shadowOf(getApplicationContext()).assertNoBroadcastListenersRegistered(realActivity, "Activity");
230    }
231
232    /**
233     * Non-Android accessor.
234     *
235     * @return the {@code contentView} set by one of the {@code setContentView()} methods
236     */
237    public View getContentView() {
238        return contentView;
239    }
240
241    /**
242     * Non-Android accessor.
243     *
244     * @return the {@code resultCode} set by one of the {@code setResult()} methods
245     */
246    public int getResultCode() {
247        return resultCode;
248    }
249
250    /**
251     * Non-Android accessor.
252     *
253     * @return the {@code Intent} set by {@link #setResult(int, android.content.Intent)}
254     */
255    public Intent getResultIntent() {
256        return resultIntent;
257    }
258
259    /**
260     * Non-Android accessor consumes and returns the next {@code Intent} on the
261     * started activities for results stack.
262     *
263     * @return the next started {@code Intent} for an activity, wrapped in
264     *         an {@link ShadowActivity.IntentForResult} object
265     */
266    public IntentForResult getNextStartedActivityForResult() {
267        if (startedActivitiesForResults.isEmpty()) {
268            return null;
269        } else {
270            return startedActivitiesForResults.remove(0);
271        }
272    }
273
274    /**
275     * Non-Android accessor returns the most recent {@code Intent} started by
276     * {@link #startActivityForResult(android.content.Intent, int)} without
277     * consuming it.
278     *
279     * @return the most recently started {@code Intent}, wrapped in
280     *         an {@link ShadowActivity.IntentForResult} object
281     */
282    public IntentForResult peekNextStartedActivityForResult() {
283        if (startedActivitiesForResults.isEmpty()) {
284            return null;
285        } else {
286            return startedActivitiesForResults.get(0);
287        }
288    }
289
290    @Implementation
291    public Object getLastNonConfigurationInstance() {
292        return lastNonConfigurationInstance;
293    }
294
295    public void setLastNonConfigurationInstance(Object lastNonConfigurationInstance) {
296        this.lastNonConfigurationInstance = lastNonConfigurationInstance;
297    }
298
299    /**
300     * Non-Android accessor Sets the {@code View} for this {@code Activity}
301     *
302     * @param view
303     */
304    public void setCurrentFocus(View view) {
305        currentFocus = view;
306    }
307
308    @Implementation
309    public View getCurrentFocus() {
310        return currentFocus;
311    }
312
313    @Implementation
314    public boolean onKeyUp(int keyCode, KeyEvent event) {
315        onKeyUpWasCalled = true;
316        if (keyCode == KeyEvent.KEYCODE_BACK) {
317            onBackPressed();
318            return true;
319        }
320        return false;
321    }
322
323    public boolean onKeyUpWasCalled() {
324        return onKeyUpWasCalled;
325    }
326
327    public void resetKeyUpWasCalled() {
328        onKeyUpWasCalled = false;
329    }
330
331    /**
332     * Container object to hold an Intent, together with the requestCode used
333     * in a call to {@code Activity#startActivityForResult(Intent, int)}
334     */
335    public class IntentForResult {
336        public Intent intent;
337        public int requestCode;
338
339        public IntentForResult(Intent intent, int requestCode) {
340            this.intent = intent;
341            this.requestCode = requestCode;
342        }
343    }
344
345    @Implementation
346    public void startActivityForResult(Intent intent, int requestCode) {
347        intentRequestCodeMap.put(intent, requestCode);
348        startedActivitiesForResults.add(new IntentForResult(intent, requestCode));
349        getApplicationContext().startActivity(intent);
350    }
351
352    public void receiveResult(Intent requestIntent, int resultCode, Intent resultIntent) {
353        Integer requestCode = intentRequestCodeMap.get(requestIntent);
354        if (requestCode == null) {
355            throw new RuntimeException("No intent matches " + requestIntent + " among " + intentRequestCodeMap.keySet());
356        }
357        try {
358            Method method = Activity.class.getDeclaredMethod("onActivityResult", Integer.TYPE, Integer.TYPE, Intent.class);
359            method.setAccessible(true);
360            method.invoke(realActivity, requestCode, resultCode, resultIntent);
361        } catch (IllegalAccessException e) {
362            throw new RuntimeException(e);
363        } catch (InvocationTargetException e) {
364            throw new RuntimeException(e);
365        } catch (NoSuchMethodException e) {
366            throw new RuntimeException(e);
367        }
368    }
369
370    @Implementation
371    public final void showDialog(int id) {
372        showDialog(id, null);
373    }
374
375    @Implementation
376    public final void removeDialog(int id) {
377        dialogForId.remove(id);
378    }
379
380    @Implementation
381    public final boolean showDialog(int id, Bundle bundle) {
382        Dialog dialog = null;
383        this.lastShownDialogId = id;
384
385        dialog = dialogForId.get(id);
386
387        if (dialog == null) {
388            try {
389                Method method = Activity.class.getDeclaredMethod("onCreateDialog", Integer.TYPE);
390                method.setAccessible(true);
391                dialog = (Dialog) method.invoke(realActivity, id);
392
393                if (bundle == null) {
394                    method = Activity.class.getDeclaredMethod("onPrepareDialog", Integer.TYPE, Dialog.class);
395                    method.setAccessible(true);
396                    method.invoke(realActivity, id, dialog);
397                } else {
398                    method = Activity.class.getDeclaredMethod("onPrepareDialog", Integer.TYPE, Dialog.class, Bundle.class);
399                    method.setAccessible(true);
400                    method.invoke(realActivity, id, dialog, bundle);
401                }
402            } catch (IllegalAccessException e) {
403                throw new RuntimeException(e);
404            } catch (InvocationTargetException e) {
405                throw new RuntimeException(e);
406            } catch (NoSuchMethodException e) {
407                throw new RuntimeException(e);
408            }
409
410            dialogForId.put(id, dialog);
411        }
412
413        dialog.show();
414
415        return true;
416    }
417
418    /**
419     * Non-Android accessor
420     *
421     * @return the dialog resource id passed into
422     *         {@code Activity#showDialog(int, Bundle)} or {@code Activity#showDialog(int)}
423     */
424    public Integer getLastShownDialogId() {
425        return lastShownDialogId;
426    }
427
428    public boolean hasCancelledPendingTransitions() {
429        return pendingTransitionEnterAnimResId == 0 && pendingTransitionExitAnimResId == 0;
430    }
431
432    @Implementation
433    public void overridePendingTransition(int enterAnim, int exitAnim) {
434        pendingTransitionEnterAnimResId = enterAnim;
435        pendingTransitionExitAnimResId = exitAnim;
436    }
437
438    public Dialog getDialogById(int dialogId) {
439    	return dialogForId.get(dialogId);
440    }
441}
442