1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.test;
18
19import android.app.Activity;
20import android.app.Application;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.ActivityInfo;
25import android.os.Bundle;
26import android.os.IBinder;
27import android.test.mock.MockApplication;
28import android.view.Window;
29import android.util.Log;
30
31
32
33/**
34 * This class provides isolated testing of a single activity.  The activity under test will
35 * be created with minimal connection to the system infrastructure, and you can inject mocked or
36 * wrappered versions of many of Activity's dependencies.  Most of the work is handled
37 * automatically here by {@link #setUp} and {@link #tearDown}.
38 *
39 * <p>If you prefer a functional test, see {@link android.test.ActivityInstrumentationTestCase}.
40 *
41 * <p>It must be noted that, as a true unit test, your Activity will not be running in the
42 * normal system and will not participate in the normal interactions with other Activities.
43 * The following methods should not be called in this configuration - most of them will throw
44 * exceptions:
45 * <ul>
46 * <li>{@link android.app.Activity#createPendingResult(int, Intent, int)}</li>
47 * <li>{@link android.app.Activity#startActivityIfNeeded(Intent, int)}</li>
48 * <li>{@link android.app.Activity#startActivityFromChild(Activity, Intent, int)}</li>
49 * <li>{@link android.app.Activity#startNextMatchingActivity(Intent)}</li>
50 * <li>{@link android.app.Activity#getCallingActivity()}</li>
51 * <li>{@link android.app.Activity#getCallingPackage()}</li>
52 * <li>{@link android.app.Activity#createPendingResult(int, Intent, int)}</li>
53 * <li>{@link android.app.Activity#getTaskId()}</li>
54 * <li>{@link android.app.Activity#isTaskRoot()}</li>
55 * <li>{@link android.app.Activity#moveTaskToBack(boolean)}</li>
56 * </ul>
57 *
58 * <p>The following methods may be called but will not do anything.  For test purposes, you can use
59 * the methods {@link #getStartedActivityIntent()} and {@link #getStartedActivityRequest()} to
60 * inspect the parameters that they were called with.
61 * <ul>
62 * <li>{@link android.app.Activity#startActivity(Intent)}</li>
63 * <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
64 * </ul>
65 *
66 * <p>The following methods may be called but will not do anything.  For test purposes, you can use
67 * the methods {@link #isFinishCalled()} and {@link #getFinishedActivityRequest()} to inspect the
68 * parameters that they were called with.
69 * <ul>
70 * <li>{@link android.app.Activity#finish()}</li>
71 * <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
72 * <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
73 * </ul>
74 *
75 */
76public abstract class ActivityUnitTestCase<T extends Activity>
77        extends ActivityTestCase {
78
79    private static final String TAG = "ActivityUnitTestCase";
80    private Class<T> mActivityClass;
81
82    private Context mActivityContext;
83    private Application mApplication;
84    private MockParent mMockParent;
85
86    private boolean mAttached = false;
87    private boolean mCreated = false;
88
89    public ActivityUnitTestCase(Class<T> activityClass) {
90        mActivityClass = activityClass;
91    }
92
93    @Override
94    public T getActivity() {
95        return (T) super.getActivity();
96    }
97
98    @Override
99    protected void setUp() throws Exception {
100        super.setUp();
101
102        // default value for target context, as a default
103      mActivityContext = getInstrumentation().getTargetContext();
104    }
105
106    /**
107     * Start the activity under test, in the same way as if it was started by
108     * {@link android.content.Context#startActivity Context.startActivity()}, providing the
109     * arguments it supplied.  When you use this method to start the activity, it will automatically
110     * be stopped by {@link #tearDown}.
111     *
112     * <p>This method will call onCreate(), but if you wish to further exercise Activity life
113     * cycle methods, you must call them yourself from your test case.
114     *
115     * <p><i>Do not call from your setUp() method.  You must call this method from each of your
116     * test methods.</i>
117     *
118     * @param intent The Intent as if supplied to {@link android.content.Context#startActivity}.
119     * @param savedInstanceState The instance state, if you are simulating this part of the life
120     * cycle.  Typically null.
121     * @param lastNonConfigurationInstance This Object will be available to the
122     * Activity if it calls {@link android.app.Activity#getLastNonConfigurationInstance()}.
123     * Typically null.
124     * @return Returns the Activity that was created
125     */
126    protected T startActivity(Intent intent, Bundle savedInstanceState,
127            Object lastNonConfigurationInstance) {
128        assertFalse("Activity already created", mCreated);
129
130        if (!mAttached) {
131            assertNotNull(mActivityClass);
132            setActivity(null);
133            T newActivity = null;
134            try {
135                IBinder token = null;
136                if (mApplication == null) {
137                    setApplication(new MockApplication());
138                }
139                ComponentName cn = new ComponentName(mActivityClass.getPackage().getName(),
140                        mActivityClass.getName());
141                intent.setComponent(cn);
142                ActivityInfo info = new ActivityInfo();
143                CharSequence title = mActivityClass.getName();
144                mMockParent = new MockParent();
145                String id = null;
146
147                newActivity = (T) getInstrumentation().newActivity(mActivityClass, mActivityContext,
148                        token, mApplication, intent, info, title, mMockParent, id,
149                        lastNonConfigurationInstance);
150            } catch (Exception e) {
151                Log.w(TAG, "Catching exception", e);
152                assertNotNull(newActivity);
153            }
154
155            assertNotNull(newActivity);
156            setActivity(newActivity);
157
158            mAttached = true;
159        }
160
161        T result = getActivity();
162        if (result != null) {
163            getInstrumentation().callActivityOnCreate(getActivity(), savedInstanceState);
164            mCreated = true;
165        }
166        return result;
167    }
168
169    @Override
170    protected void tearDown() throws Exception {
171
172        setActivity(null);
173
174        // Scrub out members - protects against memory leaks in the case where someone
175        // creates a non-static inner class (thus referencing the test case) and gives it to
176        // someone else to hold onto
177        scrubClass(ActivityInstrumentationTestCase.class);
178
179        super.tearDown();
180    }
181
182    /**
183     * Set the application for use during the test.  You must call this function before calling
184     * {@link #startActivity}.  If your test does not call this method,
185     * @param application The Application object that will be injected into the Activity under test.
186     */
187    public void setApplication(Application application) {
188        mApplication = application;
189    }
190
191    /**
192     * If you wish to inject a Mock, Isolated, or otherwise altered context, you can do so
193     * here.  You must call this function before calling {@link #startActivity}.  If you wish to
194     * obtain a real Context, as a building block, use getInstrumentation().getTargetContext().
195     */
196    public void setActivityContext(Context activityContext) {
197        mActivityContext = activityContext;
198    }
199
200    /**
201     * This method will return the value if your Activity under test calls
202     * {@link android.app.Activity#setRequestedOrientation}.
203     */
204    public int getRequestedOrientation() {
205        if (mMockParent != null) {
206            return mMockParent.mRequestedOrientation;
207        }
208        return 0;
209    }
210
211    /**
212     * This method will return the launch intent if your Activity under test calls
213     * {@link android.app.Activity#startActivity(Intent)} or
214     * {@link android.app.Activity#startActivityForResult(Intent, int)}.
215     * @return The Intent provided in the start call, or null if no start call was made.
216     */
217    public Intent getStartedActivityIntent() {
218        if (mMockParent != null) {
219            return mMockParent.mStartedActivityIntent;
220        }
221        return null;
222    }
223
224    /**
225     * This method will return the launch request code if your Activity under test calls
226     * {@link android.app.Activity#startActivityForResult(Intent, int)}.
227     * @return The request code provided in the start call, or -1 if no start call was made.
228     */
229    public int getStartedActivityRequest() {
230        if (mMockParent != null) {
231            return mMockParent.mStartedActivityRequest;
232        }
233        return 0;
234    }
235
236    /**
237     * This method will notify you if the Activity under test called
238     * {@link android.app.Activity#finish()},
239     * {@link android.app.Activity#finishFromChild(Activity)}, or
240     * {@link android.app.Activity#finishActivity(int)}.
241     * @return Returns true if one of the listed finish methods was called.
242     */
243    public boolean isFinishCalled() {
244        if (mMockParent != null) {
245            return mMockParent.mFinished;
246        }
247        return false;
248    }
249
250    /**
251     * This method will return the request code if the Activity under test called
252     * {@link android.app.Activity#finishActivity(int)}.
253     * @return The request code provided in the start call, or -1 if no finish call was made.
254     */
255    public int getFinishedActivityRequest() {
256        if (mMockParent != null) {
257            return mMockParent.mFinishedActivityRequest;
258        }
259        return 0;
260    }
261
262    /**
263     * This mock Activity represents the "parent" activity.  By injecting this, we allow the user
264     * to call a few more Activity methods, including:
265     * <ul>
266     * <li>{@link android.app.Activity#getRequestedOrientation()}</li>
267     * <li>{@link android.app.Activity#setRequestedOrientation(int)}</li>
268     * <li>{@link android.app.Activity#finish()}</li>
269     * <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
270     * <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
271     * </ul>
272     *
273     * TODO: Make this overrideable, and the unit test can look for calls to other methods
274     */
275    private static class MockParent extends Activity {
276
277        public int mRequestedOrientation = 0;
278        public Intent mStartedActivityIntent = null;
279        public int mStartedActivityRequest = -1;
280        public boolean mFinished = false;
281        public int mFinishedActivityRequest = -1;
282
283        /**
284         * Implementing in the parent allows the user to call this function on the tested activity.
285         */
286        @Override
287        public void setRequestedOrientation(int requestedOrientation) {
288            mRequestedOrientation = requestedOrientation;
289        }
290
291        /**
292         * Implementing in the parent allows the user to call this function on the tested activity.
293         */
294        @Override
295        public int getRequestedOrientation() {
296            return mRequestedOrientation;
297        }
298
299        /**
300         * By returning null here, we inhibit the creation of any "container" for the window.
301         */
302        @Override
303        public Window getWindow() {
304            return null;
305        }
306
307        /**
308         * By defining this in the parent, we allow the tested activity to call
309         * <ul>
310         * <li>{@link android.app.Activity#startActivity(Intent)}</li>
311         * <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
312         * </ul>
313         */
314        @Override
315        public void startActivityFromChild(Activity child, Intent intent, int requestCode) {
316            mStartedActivityIntent = intent;
317            mStartedActivityRequest = requestCode;
318        }
319
320        /**
321         * By defining this in the parent, we allow the tested activity to call
322         * <ul>
323         * <li>{@link android.app.Activity#finish()}</li>
324         * <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
325         * </ul>
326         */
327        @Override
328        public void finishFromChild(Activity child) {
329            mFinished = true;
330        }
331
332        /**
333         * By defining this in the parent, we allow the tested activity to call
334         * <ul>
335         * <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
336         * </ul>
337         */
338        @Override
339        public void finishActivityFromChild(Activity child, int requestCode) {
340            mFinished = true;
341            mFinishedActivityRequest = requestCode;
342        }
343    }
344}
345