1 package org.robolectric.android.controller;
2 
3 import static android.os.Build.VERSION_CODES.M;
4 import static android.os.Build.VERSION_CODES.N_MR1;
5 import static android.os.Build.VERSION_CODES.O_MR1;
6 import static android.os.Build.VERSION_CODES.P;
7 import static android.os.Build.VERSION_CODES.Q;
8 import static android.os.Build.VERSION_CODES.TIRAMISU;
9 import static com.google.common.base.Preconditions.checkNotNull;
10 import static org.robolectric.shadow.api.Shadow.extract;
11 import static org.robolectric.util.reflector.Reflector.reflector;
12 
13 import android.app.Activity;
14 import android.app.Application;
15 import android.app.Instrumentation;
16 import android.content.ComponentName;
17 import android.content.Context;
18 import android.content.Intent;
19 import android.content.pm.ActivityInfo;
20 import android.content.pm.ActivityInfo.Config;
21 import android.content.pm.PackageManager;
22 import android.content.res.Configuration;
23 import android.os.Bundle;
24 import android.util.DisplayMetrics;
25 import android.view.Display;
26 import android.view.ViewRootImpl;
27 import android.view.WindowManager;
28 import com.google.errorprone.annotations.CanIgnoreReturnValue;
29 import javax.annotation.Nullable;
30 import org.robolectric.RuntimeEnvironment;
31 import org.robolectric.shadow.api.Shadow;
32 import org.robolectric.shadows.ShadowActivity;
33 import org.robolectric.shadows.ShadowContextThemeWrapper;
34 import org.robolectric.shadows.ShadowPackageManager;
35 import org.robolectric.shadows.ShadowViewRootImpl;
36 import org.robolectric.shadows._Activity_;
37 import org.robolectric.util.ReflectionHelpers;
38 import org.robolectric.util.ReflectionHelpers.ClassParameter;
39 import org.robolectric.util.reflector.Accessor;
40 import org.robolectric.util.reflector.ForType;
41 import org.robolectric.util.reflector.WithType;
42 
43 /**
44  * ActivityController provides low-level APIs to control activity's lifecycle.
45  *
46  * <p>Using ActivityController directly from your tests is strongly discouraged. You have to call
47  * all the lifecycle callback methods (create, postCreate, start, ...) in the same manner as the
48  * Android framework by yourself otherwise you'll see fidelity issues. Consider using {@link
49  * androidx.test.core.app.ActivityScenario} instead, which provides higher-level, streamlined APIs
50  * to control the lifecycle and it works with instrumentation tests too.
51  *
52  * @param <T> a class of the activity which is under control by this class.
53  */
54 @SuppressWarnings("NewApi")
55 public class ActivityController<T extends Activity>
56     extends ComponentController<ActivityController<T>, T> implements AutoCloseable {
57 
58   enum LifecycleState {
59     INITIAL,
60     CREATED,
61     RESTARTED,
62     STARTED,
63     RESUMED,
64     PAUSED,
65     STOPPED,
66     DESTROYED
67   }
68 
69   // ActivityInfo constant.
70   private static final int CONFIG_WINDOW_CONFIGURATION = 0x20000000;
71 
72   private _Activity_ _component_;
73   private LifecycleState currentState = LifecycleState.INITIAL;
74 
of( T activity, Intent intent, @Nullable Bundle activityOptions)75   public static <T extends Activity> ActivityController<T> of(
76       T activity, Intent intent, @Nullable Bundle activityOptions) {
77     return new ActivityController<>(activity, intent).attach(activityOptions);
78   }
79 
of(T activity, Intent intent)80   public static <T extends Activity> ActivityController<T> of(T activity, Intent intent) {
81     return new ActivityController<>(activity, intent).attach(/* activityOptions= */ null);
82   }
83 
of(T activity)84   public static <T extends Activity> ActivityController<T> of(T activity) {
85     return new ActivityController<>(activity, null).attach(/* activityOptions= */ null);
86   }
87 
ActivityController(T activity, Intent intent)88   private ActivityController(T activity, Intent intent) {
89     super(activity, intent);
90 
91     _component_ = reflector(_Activity_.class, component);
92   }
93 
attach(@ullable Bundle activityOptions)94   private ActivityController<T> attach(@Nullable Bundle activityOptions) {
95     return attach(
96         activityOptions, /* lastNonConfigurationInstances= */ null, /* overrideConfig= */ null);
97   }
98 
attach( @ullable Bundle activityOptions, @Nullable @WithType("android.app.Activity$NonConfigurationInstances") Object lastNonConfigurationInstances, @Nullable Configuration overrideConfig)99   private ActivityController<T> attach(
100       @Nullable Bundle activityOptions,
101       @Nullable @WithType("android.app.Activity$NonConfigurationInstances")
102           Object lastNonConfigurationInstances,
103       @Nullable Configuration overrideConfig) {
104     if (attached) {
105       return this;
106     }
107     // make sure the component is enabled
108     Context context = RuntimeEnvironment.getApplication().getBaseContext();
109     PackageManager packageManager = context.getPackageManager();
110     ComponentName componentName =
111         new ComponentName(context.getPackageName(), this.component.getClass().getName());
112     ((ShadowPackageManager) extract(packageManager)).addActivityIfNotPresent(componentName);
113     packageManager.setComponentEnabledSetting(
114         componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
115     ShadowActivity shadowActivity = Shadow.extract(component);
116     shadowActivity.callAttach(
117         getIntent(), activityOptions, lastNonConfigurationInstances, overrideConfig);
118     shadowActivity.attachController(this);
119     attached = true;
120     return this;
121   }
122 
getActivityInfo(Application application)123   private ActivityInfo getActivityInfo(Application application) {
124     PackageManager packageManager = application.getPackageManager();
125     ComponentName componentName =
126         new ComponentName(application.getPackageName(), this.component.getClass().getName());
127     try {
128       return packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA);
129     } catch (PackageManager.NameNotFoundException e) {
130       throw new RuntimeException(e);
131     }
132   }
133 
create(@ullable final Bundle bundle)134   public ActivityController<T> create(@Nullable final Bundle bundle) {
135     shadowMainLooper.runPaused(
136         () -> {
137           getInstrumentation().callActivityOnCreate(component, bundle);
138           currentState = LifecycleState.CREATED;
139         });
140     return this;
141   }
142 
143   @Override
create()144   public ActivityController<T> create() {
145     return create(null);
146   }
147 
restart()148   public ActivityController<T> restart() {
149     invokeWhilePaused(
150         () -> {
151           if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
152             _component_.performRestart();
153           } else if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
154             _component_.performRestart(true, "restart()");
155           } else {
156             _component_.performRestart(true);
157           }
158           currentState = LifecycleState.RESTARTED;
159         });
160     return this;
161   }
162 
start()163   public ActivityController<T> start() {
164     // Start and stop are tricky cases. Unlike other lifecycle methods such as
165     // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls
166     // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite.
167     invokeWhilePaused(
168         () -> {
169           if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
170             _component_.performStart();
171           } else {
172             _component_.performStart("start()");
173           }
174           currentState = LifecycleState.STARTED;
175         });
176     return this;
177   }
178 
restoreInstanceState(Bundle bundle)179   public ActivityController<T> restoreInstanceState(Bundle bundle) {
180     shadowMainLooper.runPaused(
181         () -> getInstrumentation().callActivityOnRestoreInstanceState(component, bundle));
182     return this;
183   }
184 
postCreate(@ullable Bundle bundle)185   public ActivityController<T> postCreate(@Nullable Bundle bundle) {
186     invokeWhilePaused(() -> _component_.onPostCreate(bundle));
187     return this;
188   }
189 
resume()190   public ActivityController<T> resume() {
191     invokeWhilePaused(
192         () -> {
193           if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
194             _component_.performResume();
195           } else {
196             _component_.performResume(true, "resume()");
197           }
198           currentState = LifecycleState.RESUMED;
199         });
200     return this;
201   }
202 
postResume()203   public ActivityController<T> postResume() {
204     invokeWhilePaused(() -> _component_.onPostResume());
205     return this;
206   }
207 
208   /**
209    * Calls the same lifecycle methods on the Activity called by Android when an Activity is the top
210    * most resumed activity on Q+.
211    */
212   @CanIgnoreReturnValue
topActivityResumed(boolean isTop)213   public ActivityController<T> topActivityResumed(boolean isTop) {
214     if (RuntimeEnvironment.getApiLevel() < Q) {
215       return this;
216     }
217     invokeWhilePaused(
218         () -> _component_.performTopResumedActivityChanged(isTop, "topStateChangedWhenResumed"));
219     return this;
220   }
221 
visible()222   public ActivityController<T> visible() {
223     shadowMainLooper.runPaused(
224         () -> {
225           // emulate logic of ActivityThread#handleResumeActivity
226           component.getWindow().getAttributes().type =
227               WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
228           _component_.setDecor(component.getWindow().getDecorView());
229           _component_.makeVisible();
230         });
231 
232     shadowMainLooper.idleIfPaused();
233     ViewRootImpl root = getViewRoot();
234     // root can be null if activity does not have content attached, or if looper is paused.
235     // this is unusual but leave the check here for legacy compatibility
236     if (root != null) {
237       shadowMainLooper.idleIfPaused();
238     }
239     return this;
240   }
241 
getViewRoot()242   private ViewRootImpl getViewRoot() {
243     return component.getWindow().getDecorView().getViewRootImpl();
244   }
245 
callDispatchResized(ViewRootImpl root)246   private void callDispatchResized(ViewRootImpl root) {
247     ((ShadowViewRootImpl) extract(root)).callDispatchResized();
248   }
249 
windowFocusChanged(boolean hasFocus)250   public ActivityController<T> windowFocusChanged(boolean hasFocus) {
251     ViewRootImpl root = getViewRoot();
252     if (root == null) {
253       // root can be null if looper was paused during visible. Flush the looper and try again
254       shadowMainLooper.idle();
255 
256       root = checkNotNull(getViewRoot());
257       callDispatchResized(root);
258     }
259 
260     ((ShadowViewRootImpl) extract(root)).callWindowFocusChanged(hasFocus);
261 
262     shadowMainLooper.idleIfPaused();
263     return this;
264   }
265 
userLeaving()266   public ActivityController<T> userLeaving() {
267     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnUserLeaving(component));
268     return this;
269   }
270 
pause()271   public ActivityController<T> pause() {
272     shadowMainLooper.runPaused(
273         () -> {
274           getInstrumentation().callActivityOnPause(component);
275           currentState = LifecycleState.PAUSED;
276         });
277     return this;
278   }
279 
saveInstanceState(Bundle outState)280   public ActivityController<T> saveInstanceState(Bundle outState) {
281     shadowMainLooper.runPaused(
282         () -> getInstrumentation().callActivityOnSaveInstanceState(component, outState));
283     return this;
284   }
285 
stop()286   public ActivityController<T> stop() {
287     // Stop and start are tricky cases. Unlike other lifecycle methods such as
288     // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls
289     // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite.
290     invokeWhilePaused(
291         () -> {
292           if (RuntimeEnvironment.getApiLevel() <= M) {
293             _component_.performStop();
294           } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
295             _component_.performStop(true);
296           } else {
297             _component_.performStop(true, "stop()");
298           }
299           currentState = LifecycleState.STOPPED;
300         });
301     return this;
302   }
303 
304   @Override
destroy()305   public ActivityController<T> destroy() {
306     shadowMainLooper.runPaused(
307         () -> {
308           getInstrumentation().callActivityOnDestroy(component);
309           makeActivityEligibleForGc();
310           currentState = LifecycleState.DESTROYED;
311         });
312     return this;
313   }
314 
makeActivityEligibleForGc()315   private void makeActivityEligibleForGc() {
316     // Clear WindowManager state for this activity. On real Android this is done by
317     // ActivityThread.handleDestroyActivity, which is initiated by the window manager
318     // service.
319     boolean windowAdded = _component_.getWindowAdded();
320     if (windowAdded) {
321       WindowManager windowManager = component.getWindowManager();
322       windowManager.removeViewImmediate(component.getWindow().getDecorView());
323     }
324     if (RuntimeEnvironment.getApiLevel() >= O_MR1) {
325       // Starting Android O_MR1, there is a leak in Android where `ContextImpl` holds on to the
326       // activity after being destroyed. This "fixes" the leak in Robolectric only, and will be
327       // properly fixed in Android S.
328       component.setAutofillClient(null);
329     }
330   }
331 
332   /**
333    * Calls the same lifecycle methods on the Activity called by Android the first time the Activity
334    * is created.
335    *
336    * @return Activity controller instance.
337    */
setup()338   public ActivityController<T> setup() {
339     return create().start().postCreate(null).resume().visible().topActivityResumed(true);
340   }
341 
342   /**
343    * Calls the same lifecycle methods on the Activity called by Android when an Activity is restored
344    * from previously saved state.
345    *
346    * @param savedInstanceState Saved instance state.
347    * @return Activity controller instance.
348    */
setup(Bundle savedInstanceState)349   public ActivityController<T> setup(Bundle savedInstanceState) {
350     return create(savedInstanceState)
351         .start()
352         .restoreInstanceState(savedInstanceState)
353         .postCreate(savedInstanceState)
354         .resume()
355         .visible()
356         .topActivityResumed(true);
357   }
358 
newIntent(Intent intent)359   public ActivityController<T> newIntent(Intent intent) {
360     invokeWhilePaused(() -> _component_.onNewIntent(intent));
361     return this;
362   }
363 
364   /**
365    * Performs a configuration change on the Activity. See {@link #configurationChange(Configuration,
366    * DisplayMetrics, int)}. The configuration is taken from the application's configuration.
367    */
368   @CanIgnoreReturnValue
configurationChange()369   public ActivityController<T> configurationChange() {
370     return configurationChange(component.getApplicationContext().getResources().getConfiguration());
371   }
372 
373   /**
374    * Performs a configuration change on the Activity. See {@link #configurationChange(Configuration,
375    * DisplayMetrics, int)}. The changed configuration is calculated based on the activity's existing
376    * configuration.
377    */
378   @CanIgnoreReturnValue
configurationChange(final Configuration newConfiguration)379   public ActivityController<T> configurationChange(final Configuration newConfiguration) {
380     return configurationChange(newConfiguration, component.getResources().getDisplayMetrics());
381   }
382 
383   /**
384    * Performs a configuration change on the Activity.
385    *
386    * <p>If the activity is configured to handle changes without being recreated, {@link
387    * Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity is
388    * recreated as described <a
389    * href="https://developer.android.com/guide/topics/resources/runtime-changes.html">here</a>.
390    *
391    * <p>Typically configuration should be applied using {@link RuntimeEnvironment#setQualifiers} and
392    * then propagated to the activity controller, e.g.
393    *
394    * <pre>{@code
395    * RuntimeEnvironment.setQualifiers("+ar-rXB");
396    * activityController.configurationChange();
397    * }</pre>
398    *
399    * @param newConfiguration The new configuration to be set.
400    * @return ActivityController instance
401    */
402   @CanIgnoreReturnValue
configurationChange( Configuration newConfiguration, DisplayMetrics newMetrics)403   public ActivityController<T> configurationChange(
404       Configuration newConfiguration, DisplayMetrics newMetrics) {
405     ActivityReflector activityReflector = reflector(ActivityReflector.class, component);
406     Configuration currentConfig =
407         System.getProperty("robolectric.configurationChangeFix", "true").equals("true")
408             ? activityReflector.getCurrentConfig()
409             : component.getResources().getConfiguration();
410     return configurationChange(newConfiguration, newMetrics, currentConfig.diff(newConfiguration));
411   }
412 
413   /**
414    * Performs a configuration change on the Activity.
415    *
416    * <p>If the activity is configured to handle changes without being recreated, {@link
417    * Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity is
418    * recreated as described <a
419    * href="https://developer.android.com/guide/topics/resources/runtime-changes.html">here</a>.
420    *
421    * <p>Typically configuration should be applied using {@link RuntimeEnvironment#setQualifiers} and
422    * then propagated to the activity controller, e.g.
423    *
424    * <pre>{@code
425    * Resources resources = RuntimeEnvironment.getApplication().getResources();
426    * Configuration oldConfig = new Configuration(resources.getConfiguration());
427    * RuntimeEnvironment.setQualifiers("+ar-rXB");
428    * Configuration newConfig = resources.getConfiguration();
429    * activityController.configurationChange(
430    *     newConfig, resources.getDisplayMetrics(), oldConfig.diff(newConfig));
431    * }</pre>
432    *
433    * @param newConfiguration The new configuration to be set.
434    * @param changedConfig The changed configuration properties bitmask (e.g. the result of calling
435    *     {@link Configuration#diff(Configuration)}). This will be used to determine whether the
436    *     activity handles the configuration change or not, and whether it must be recreated.
437    * @return ActivityController instance
438    * @deprecated The config change should be calculated internally by the activity controller based
439    *     on the previous configuration, use {@link #configurationChange(Configuration,
440    *     DisplayMetrics)} instead.
441    */
442   @Deprecated
443   @CanIgnoreReturnValue
configurationChange( Configuration newConfiguration, DisplayMetrics newMetrics, @Config int changedConfig)444   public ActivityController<T> configurationChange(
445       Configuration newConfiguration, DisplayMetrics newMetrics, @Config int changedConfig) {
446     component.getResources().updateConfiguration(newConfiguration, newMetrics);
447 
448     int filteredChanges = filterConfigChanges(changedConfig);
449     // TODO: throw on changedConfig == 0 since it non-intuitively calls onConfigurationChanged
450 
451     // Can the activity handle itself ALL configuration changes?
452     if ((getActivityInfo(component.getApplication()).configChanges & filteredChanges)
453         == filteredChanges) {
454       shadowMainLooper.runPaused(
455           () -> {
456             reflector(ActivityReflector.class, component)
457                 .getCurrentConfig()
458                 .setTo(newConfiguration);
459             component.onConfigurationChanged(newConfiguration);
460             ViewRootImpl root = getViewRoot();
461             if (root != null) {
462               if (RuntimeEnvironment.getApiLevel() <= N_MR1) {
463                 ReflectionHelpers.callInstanceMethod(
464                     root,
465                     "updateConfiguration",
466                     ClassParameter.from(Configuration.class, newConfiguration),
467                     ClassParameter.from(boolean.class, false));
468               } else {
469                 root.updateConfiguration(Display.INVALID_DISPLAY);
470               }
471             }
472           });
473 
474       return this;
475     } else {
476       @SuppressWarnings("unchecked")
477       final T recreatedActivity = (T) ReflectionHelpers.callConstructor(component.getClass());
478       final _Activity_ _recreatedActivity_ = reflector(_Activity_.class, recreatedActivity);
479 
480       shadowMainLooper.runPaused(
481           () -> {
482             // Set flags
483             _component_.setChangingConfigurations(true);
484             _component_.setConfigChangeFlags(filteredChanges);
485 
486             // Perform activity destruction
487             final Bundle outState = new Bundle();
488 
489             // The order of onPause/onStop/onSaveInstanceState is undefined, but is usually:
490             // onPause -> onSaveInstanceState -> onStop before API P, and onPause -> onStop ->
491             // onSaveInstanceState from API P.
492             // See
493             // https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle) for documentation explained.
494             // And see ActivityThread#callActivityOnStop for related code.
495             getInstrumentation().callActivityOnPause(component);
496             if (RuntimeEnvironment.getApiLevel() < P) {
497               _component_.performSaveInstanceState(outState);
498               if (RuntimeEnvironment.getApiLevel() <= M) {
499                 _component_.performStop();
500               } else {
501                 // API from N to O_MR1(both including)
502                 _component_.performStop(true);
503               }
504             } else {
505               _component_.performStop(true, "configurationChange");
506               _component_.performSaveInstanceState(outState);
507             }
508 
509             // This is the true and complete retained state, including loaders and retained
510             // fragments.
511             final Object nonConfigInstance = _component_.retainNonConfigurationInstances();
512             // This is the activity's "user" state
513             final Object activityConfigInstance =
514                 nonConfigInstance == null
515                     ? null // No framework or user state.
516                     : reflector(_NonConfigurationInstances_.class, nonConfigInstance).getActivity();
517 
518             getInstrumentation().callActivityOnDestroy(component);
519             makeActivityEligibleForGc();
520 
521             // Restore theme in case it was set in the test manually.
522             // This is not technically what happens but is purely to make this easier to use in
523             // Robolectric.
524             ShadowContextThemeWrapper shadowContextThemeWrapper = Shadow.extract(component);
525             int theme = shadowContextThemeWrapper.callGetThemeResId();
526 
527             // Setup controller for the new activity
528             attached = false;
529             component = recreatedActivity;
530             _component_ = _recreatedActivity_;
531 
532             // TODO: Because robolectric is currently not creating unique context objects per
533             //  activity and that the app copmat framework uses weak maps to cache resources per
534             //  context the caches end up with stale objects between activity creations (which would
535             //  typically be flushed by an onConfigurationChanged when running in real android). To
536             //  workaround this we can invoke a gc after running the configuration change and
537             //  destroying the old activity which will flush the object references from the weak
538             //  maps (the side effect otherwise is flaky tests that behave differently based on when
539             //  garbage collection last happened to run).
540             //  This should be removed when robolectric.createActivityContexts is enabled.
541             System.gc();
542 
543             // TODO: Pass nonConfigurationInstance here instead of setting
544             // mLastNonConfigurationInstances directly below. This field must be set before
545             // attach. Since current implementation sets it after attach(), initialization is not
546             // done correctly. For instance, fragment marked as retained is not retained.
547             attach(
548                 /* activityOptions= */ null,
549                 /* lastNonConfigurationInstances= */ null,
550                 newConfiguration);
551 
552             if (theme != 0) {
553               recreatedActivity.setTheme(theme);
554             }
555 
556             // Set saved non config instance
557             _recreatedActivity_.setLastNonConfigurationInstances(nonConfigInstance);
558             ShadowActivity shadowActivity = Shadow.extract(recreatedActivity);
559             shadowActivity.setLastNonConfigurationInstance(activityConfigInstance);
560 
561             // Create lifecycle
562             getInstrumentation().callActivityOnCreate(recreatedActivity, outState);
563 
564             if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
565 
566               _recreatedActivity_.performStart();
567 
568             } else {
569               _recreatedActivity_.performStart("configurationChange");
570             }
571 
572             getInstrumentation().callActivityOnRestoreInstanceState(recreatedActivity, outState);
573             _recreatedActivity_.onPostCreate(outState);
574             if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
575               _recreatedActivity_.performResume();
576             } else {
577               _recreatedActivity_.performResume(true, "configurationChange");
578             }
579             _recreatedActivity_.onPostResume();
580             // TODO: Call visible() too.
581             if (RuntimeEnvironment.getApiLevel() >= Q) {
582               _recreatedActivity_.performTopResumedActivityChanged(true, "configurationChange");
583             }
584           });
585     }
586 
587     return this;
588   }
589 
590   /**
591    * Recreates activity instance which is controlled by this ActivityController.
592    * NonConfigurationInstances and savedInstanceStateBundle are properly passed into a new instance.
593    * After the recreation, it brings back its lifecycle state to the original state. The activity
594    * should not be destroyed yet.
595    */
596   @SuppressWarnings("unchecked")
recreate()597   public ActivityController<T> recreate() {
598 
599     LifecycleState originalState = currentState;
600 
601     switch (originalState) {
602       case INITIAL:
603         create();
604       // fall through
605       case CREATED:
606       case RESTARTED:
607         start();
608         postCreate(null);
609       // fall through
610       case STARTED:
611         resume();
612       // fall through
613       default:
614         // fall through
615     }
616 
617     // Activity#mChangingConfigurations flag should be set prior to Activity recreation process
618     // starts. ActivityThread does set it on real device but here we simulate the Activity
619     // recreation process on behalf of ActivityThread so set the flag here. Note we don't need to
620     // reset the flag to false because this Activity instance is going to be destroyed and disposed.
621     // https://android.googlesource.com/platform/frameworks/base/+/55418eada51d4f5e6532ae9517af66c50
622     // ea495c4/core/java/android/app/ActivityThread.java#4806
623     _component_.setChangingConfigurations(true);
624 
625     switch (originalState) {
626       case INITIAL:
627       case CREATED:
628       case RESTARTED:
629       case STARTED:
630       case RESUMED:
631         pause();
632       // fall through
633       case PAUSED:
634         stop();
635       // fall through
636       case STOPPED:
637         break;
638       default:
639         throw new IllegalStateException("Cannot recreate activity since it's destroyed already");
640     }
641 
642     Bundle outState = new Bundle();
643     saveInstanceState(outState);
644     Object lastNonConfigurationInstances = _component_.retainNonConfigurationInstances();
645     Configuration overrideConfig = component.getResources().getConfiguration();
646     destroy();
647 
648     component = (T) ReflectionHelpers.callConstructor(component.getClass());
649     _component_ = reflector(_Activity_.class, component);
650     attached = false;
651     attach(/* activityOptions= */ null, lastNonConfigurationInstances, overrideConfig);
652     create(outState);
653     start();
654     restoreInstanceState(outState);
655     postCreate(outState);
656     resume();
657     postResume();
658     visible();
659     windowFocusChanged(true);
660     topActivityResumed(true);
661 
662     // Move back to the original stage. If the original stage was transient stage, it will bring it
663     // to resumed state to match the on device behavior.
664     switch (originalState) {
665       case PAUSED:
666         pause();
667         return this;
668       case STOPPED:
669         pause();
670         stop();
671         return this;
672       default:
673         return this;
674     }
675   }
676 
677   // Get the Instrumentation object scoped to the Activity.
getInstrumentation()678   private Instrumentation getInstrumentation() {
679     return _component_.getInstrumentation();
680   }
681 
682   /**
683    * Transitions the underlying Activity to the 'destroyed' state by progressing through the
684    * appropriate lifecycle events. It frees up any resources and makes the Activity eligible for GC.
685    */
686   @Override
close()687   public void close() {
688 
689     LifecycleState originalState = currentState;
690 
691     switch (originalState) {
692       case INITIAL:
693       case DESTROYED:
694         return;
695       case RESUMED:
696         pause();
697       // fall through
698       case PAUSED:
699       // fall through
700       case RESTARTED:
701       // fall through
702       case STARTED:
703         stop();
704       // fall through
705       case STOPPED:
706       // fall through
707       case CREATED:
708         break;
709     }
710 
711     destroy();
712   }
713 
714   // See ActivityRecord#getConfigurationChanges for the config changes that are considered for
715   // activity recreation by the window manager.
filterConfigChanges(int changedConfig)716   private static int filterConfigChanges(int changedConfig) {
717     // We don't want window configuration to cause relaunches.
718     if ((changedConfig & CONFIG_WINDOW_CONFIGURATION) != 0) {
719       changedConfig &= ~CONFIG_WINDOW_CONFIGURATION;
720     }
721     return changedConfig;
722   }
723 
724   /** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */
725   @ForType(className = "android.app.Activity$NonConfigurationInstances")
726   interface _NonConfigurationInstances_ {
727 
728     @Accessor("activity")
getActivity()729     Object getActivity();
730   }
731 
732   @ForType(Activity.class)
733   interface ActivityReflector {
734     @Accessor("mCurrentConfig")
getCurrentConfig()735     Configuration getCurrentConfig();
736   }
737 }
738