• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.android.controller;
2 
3 import static android.os.Build.VERSION_CODES.M;
4 import static android.os.Build.VERSION_CODES.O_MR1;
5 import static com.google.common.base.Preconditions.checkNotNull;
6 import static org.robolectric.shadow.api.Shadow.extract;
7 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
8 
9 import android.app.Activity;
10 import android.app.ActivityThread;
11 import android.app.Application;
12 import android.app.Instrumentation;
13 import android.content.ComponentName;
14 import android.content.Context;
15 import android.content.Intent;
16 import android.content.pm.ActivityInfo;
17 import android.content.pm.PackageManager;
18 import android.content.res.Configuration;
19 import android.os.Bundle;
20 import android.view.ViewRootImpl;
21 import org.robolectric.RuntimeEnvironment;
22 import org.robolectric.shadow.api.Shadow;
23 import org.robolectric.shadows.ShadowActivity;
24 import org.robolectric.shadows.ShadowContextThemeWrapper;
25 import org.robolectric.shadows.ShadowViewRootImpl;
26 import org.robolectric.util.ReflectionHelpers;
27 
28 public class ActivityController<T extends Activity> extends ComponentController<ActivityController<T>, T> {
29 
of(T activity, Intent intent)30   public static <T extends Activity> ActivityController<T> of(T activity, Intent intent) {
31     return new ActivityController<>(activity, intent).attach();
32   }
33 
of(T activity)34   public static <T extends Activity> ActivityController<T> of(T activity) {
35     return new ActivityController<>(activity, null).attach();
36   }
37 
ActivityController(T activity, Intent intent)38   private ActivityController(T activity, Intent intent) {
39     super(activity, intent);
40   }
41 
attach()42   private ActivityController<T> attach() {
43     if (attached) {
44       return this;
45     }
46     // make sure the component is enabled
47     Context context = RuntimeEnvironment.application.getBaseContext();
48     context
49         .getPackageManager()
50         .setComponentEnabledSetting(
51             new ComponentName(context.getPackageName(), component.getClass().getName()),
52             PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
53             0);
54     ShadowActivity shadowActivity = Shadow.extract(component);
55     shadowActivity.callAttach(getIntent());
56     attached = true;
57     return this;
58   }
59 
getActivityInfo(Application application)60   private ActivityInfo getActivityInfo(Application application) {
61     try {
62       return application.getPackageManager().getActivityInfo(new ComponentName(application.getPackageName(), component.getClass().getName()), PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
63     } catch (PackageManager.NameNotFoundException e) {
64       throw new RuntimeException(e);
65     }
66   }
67 
create(final Bundle bundle)68   public ActivityController<T> create(final Bundle bundle) {
69     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle));
70     return this;
71   }
72 
create()73   @Override public ActivityController<T> create() {
74     return create(null);
75   }
76 
restart()77   public ActivityController<T> restart() {
78     if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
79       invokeWhilePaused("performRestart");
80     } else {
81       invokeWhilePaused("performRestart",
82           from(boolean.class, true),
83           from(String.class, "restart()"));
84     }
85     return this;
86   }
87 
start()88   public ActivityController<T> start() {
89     // Start and stop are tricky cases. Unlike other lifecycle methods such as
90     // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls
91     // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite.
92     if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
93       invokeWhilePaused("performStart");
94     } else {
95       invokeWhilePaused("performStart", from(String.class, "start()"));
96     }
97     return this;
98   }
99 
restoreInstanceState(Bundle bundle)100   public ActivityController<T> restoreInstanceState(Bundle bundle) {
101     shadowMainLooper.runPaused(
102         () -> getInstrumentation().callActivityOnRestoreInstanceState(component, bundle));
103     return this;
104   }
105 
postCreate(Bundle bundle)106   public ActivityController<T> postCreate(Bundle bundle) {
107     invokeWhilePaused("onPostCreate", from(Bundle.class, bundle));
108     return this;
109   }
110 
resume()111   public ActivityController<T> resume() {
112     if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
113       invokeWhilePaused("performResume");
114     } else {
115       invokeWhilePaused("performResume",
116           from(boolean.class, true),
117           from(String.class, "resume()"));
118     }
119     return this;
120   }
121 
postResume()122   public ActivityController<T> postResume() {
123     invokeWhilePaused("onPostResume");
124     return this;
125   }
126 
visible()127   public ActivityController<T> visible() {
128     shadowMainLooper.runPaused(new Runnable() {
129       @Override
130       public void run() {
131         ReflectionHelpers.setField(component, "mDecor", component.getWindow().getDecorView());
132         ReflectionHelpers.callInstanceMethod(component, "makeVisible");
133       }
134     });
135 
136     ViewRootImpl root = getViewRoot();
137     // root can be null if activity does not have content attached, or if looper is paused.
138     // this is unusual but leave the check here for legacy compatibility
139     if (root != null) {
140       callDispatchResized(root);
141     }
142     return this;
143   }
144 
getViewRoot()145   private ViewRootImpl getViewRoot() {
146     return component.getWindow().getDecorView().getViewRootImpl();
147   }
148 
callDispatchResized(ViewRootImpl root)149   private void callDispatchResized(ViewRootImpl root) {
150     ((ShadowViewRootImpl) extract(root)).callDispatchResized();
151   }
152 
windowFocusChanged(boolean hasFocus)153   public ActivityController<T> windowFocusChanged(boolean hasFocus) {
154     ViewRootImpl root = getViewRoot();
155     if (root == null) {
156       // root can be null if looper was paused during visible. Flush the looper and try again
157       shadowMainLooper.idle();
158 
159       root = checkNotNull(getViewRoot());
160       callDispatchResized(root);
161     }
162 
163     ReflectionHelpers.callInstanceMethod(root, "windowFocusChanged",
164         from(boolean.class, hasFocus), /* hasFocus */
165         from(boolean.class, false) /* inTouchMode */);
166     return this;
167   }
168 
userLeaving()169   public ActivityController<T> userLeaving() {
170     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnUserLeaving(component));
171     return this;
172   }
173 
pause()174   public ActivityController<T> pause() {
175     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component));
176     return this;
177   }
178 
saveInstanceState(Bundle outState)179   public ActivityController<T> saveInstanceState(Bundle outState) {
180     shadowMainLooper.runPaused(
181         () -> getInstrumentation().callActivityOnSaveInstanceState(component, outState));
182     return this;
183   }
184 
stop()185   public ActivityController<T> stop() {
186     // Stop and start are tricky cases. Unlike other lifecycle methods such as
187     // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls
188     // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite.
189     if (RuntimeEnvironment.getApiLevel() <= M) {
190       invokeWhilePaused("performStop");
191     } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
192       invokeWhilePaused("performStop", from(boolean.class, true));
193     } else {
194       invokeWhilePaused("performStop", from(boolean.class, true), from(String.class, "stop()"));
195     }
196     return this;
197   }
198 
destroy()199   @Override public ActivityController<T> destroy() {
200     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnDestroy(component));
201     return this;
202   }
203 
204   /**
205    * Calls the same lifecycle methods on the Activity called by Android the first time the Activity is created.
206    *
207    * @return Activity controller instance.
208    */
setup()209   public ActivityController<T> setup() {
210     return create().start().postCreate(null).resume().visible();
211   }
212 
213   /**
214    * Calls the same lifecycle methods on the Activity called by Android when an Activity is restored from previously saved state.
215    *
216    * @param savedInstanceState Saved instance state.
217    * @return Activity controller instance.
218    */
setup(Bundle savedInstanceState)219   public ActivityController<T> setup(Bundle savedInstanceState) {
220     return create(savedInstanceState)
221         .start()
222         .restoreInstanceState(savedInstanceState)
223         .postCreate(savedInstanceState)
224         .resume()
225         .visible();
226   }
227 
newIntent(Intent intent)228   public ActivityController<T> newIntent(Intent intent) {
229     invokeWhilePaused("onNewIntent", from(Intent.class, intent));
230     return this;
231   }
232 
233   /**
234    * Applies the current system configuration to the Activity.
235    *
236    * This can be used in conjunction with {@link RuntimeEnvironment#setQualifiers(String)} to
237    * simulate configuration changes.
238    *
239    * If the activity is configured to handle changes without being recreated,
240    * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity
241    * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html).
242    *
243    * @return ActivityController instance
244    */
configurationChange()245   public ActivityController<T> configurationChange() {
246     return configurationChange(component.getApplicationContext().getResources().getConfiguration());
247   }
248 
249   /**
250    * Performs a configuration change on the Activity.
251    *
252    * If the activity is configured to handle changes without being recreated,
253    * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity
254    * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html).
255    *
256    * @param newConfiguration The new configuration to be set.
257    * @return ActivityController instance
258    */
configurationChange(final Configuration newConfiguration)259   public ActivityController<T> configurationChange(final Configuration newConfiguration) {
260     final Configuration currentConfig = component.getResources().getConfiguration();
261     final int changedBits = currentConfig.diff(newConfiguration);
262     currentConfig.setTo(newConfiguration);
263 
264     // TODO: throw on changedBits == 0 since it non-intuitively calls onConfigurationChanged
265 
266     // Can the activity handle itself ALL configuration changes?
267     if ((getActivityInfo(component.getApplication()).configChanges & changedBits) == changedBits) {
268       shadowMainLooper.runPaused(new Runnable() {
269         @Override
270         public void run() {
271           ReflectionHelpers.callInstanceMethod(Activity.class, component, "onConfigurationChanged",
272             from(Configuration.class, newConfiguration));
273         }
274       });
275 
276       return this;
277     } else {
278       @SuppressWarnings("unchecked")
279       final T recreatedActivity = (T) ReflectionHelpers.callConstructor(component.getClass());
280 
281       shadowMainLooper.runPaused(
282           new Runnable() {
283             @Override
284             public void run() {
285               // Set flags
286               ReflectionHelpers.setField(
287                   Activity.class, component, "mChangingConfigurations", true);
288               ReflectionHelpers.setField(
289                   Activity.class, component, "mConfigChangeFlags", changedBits);
290 
291               // Perform activity destruction
292               final Bundle outState = new Bundle();
293 
294               // The order of onPause/onStop/onSaveInstanceState is undefined, but is usually:
295               // onPause -> onSaveInstanceState -> onStop
296               ReflectionHelpers.callInstanceMethod(Activity.class, component, "performPause");
297               ReflectionHelpers.callInstanceMethod(
298                   Activity.class,
299                   component,
300                   "performSaveInstanceState",
301                   from(Bundle.class, outState));
302               if (RuntimeEnvironment.getApiLevel() <= M) {
303                 ReflectionHelpers.callInstanceMethod(Activity.class, component, "performStop");
304               } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
305                 ReflectionHelpers.callInstanceMethod(
306                     Activity.class, component, "performStop", from(boolean.class, true));
307               } else {
308                 ReflectionHelpers.callInstanceMethod(
309                     Activity.class,
310                     component,
311                     "performStop",
312                     from(boolean.class, true),
313                     from(String.class, "configurationChange"));
314               }
315 
316               // This is the true and complete retained state, including loaders and retained
317               // fragments.
318               final Object nonConfigInstance =
319                   ReflectionHelpers.callInstanceMethod(
320                       Activity.class, component, "retainNonConfigurationInstances");
321               // This is the activity's "user" state
322               final Object activityConfigInstance =
323                   nonConfigInstance == null
324                       ? null // No framework or user state.
325                       : ReflectionHelpers.getField(nonConfigInstance, "activity");
326 
327               ReflectionHelpers.callInstanceMethod(Activity.class, component, "performDestroy");
328 
329               // Restore theme in case it was set in the test manually.
330               // This is not technically what happens but is purely to make this easier to use in
331               // Robolectric.
332               ShadowContextThemeWrapper shadowContextThemeWrapper = Shadow.extract(component);
333               int theme = shadowContextThemeWrapper.callGetThemeResId();
334 
335               // Setup controller for the new activity
336               attached = false;
337               component = recreatedActivity;
338               attach();
339 
340               if (theme != 0) {
341                 recreatedActivity.setTheme(theme);
342               }
343 
344               // Set saved non config instance
345               ReflectionHelpers.setField(
346                   recreatedActivity, "mLastNonConfigurationInstances", nonConfigInstance);
347               ShadowActivity shadowActivity = Shadow.extract(recreatedActivity);
348               shadowActivity.setLastNonConfigurationInstance(activityConfigInstance);
349 
350               // Create lifecycle
351               ReflectionHelpers.callInstanceMethod(
352                   Activity.class, recreatedActivity, "performCreate", from(Bundle.class, outState));
353 
354               if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
355 
356                 ReflectionHelpers.callInstanceMethod(
357                     Activity.class, recreatedActivity, "performStart");
358 
359               } else {
360                 ReflectionHelpers.callInstanceMethod(
361                     Activity.class,
362                     recreatedActivity,
363                     "performStart",
364                     from(String.class, "configurationChange"));
365               }
366 
367               ReflectionHelpers.callInstanceMethod(
368                   Activity.class,
369                   recreatedActivity,
370                   "performRestoreInstanceState",
371                   from(Bundle.class, outState));
372               ReflectionHelpers.callInstanceMethod(
373                   Activity.class, recreatedActivity, "onPostCreate", from(Bundle.class, outState));
374               if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
375                 ReflectionHelpers.callInstanceMethod(
376                     Activity.class, recreatedActivity, "performResume");
377               } else {
378                 ReflectionHelpers.callInstanceMethod(
379                     Activity.class,
380                     recreatedActivity,
381                     "performResume",
382                     from(boolean.class, true),
383                     from(String.class, "configurationChange"));
384               }
385               ReflectionHelpers.callInstanceMethod(
386                   Activity.class, recreatedActivity, "onPostResume");
387               // TODO: Call visible() too.
388             }
389           });
390     }
391 
392     return this;
393   }
394 
getInstrumentation()395   private static Instrumentation getInstrumentation() {
396     return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation();
397   }
398 }
399