All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.android.controller.ActivityController Maven / Gradle / Ivy

There is a newer version: 4.14.1
Show newest version
package org.robolectric.android.controller;

import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O_MR1;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.app.Activity;
import android.app.ActivityThread;
import android.app.Application;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivity;
import org.robolectric.shadows.ShadowContextThemeWrapper;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowViewRootImpl;
import org.robolectric.shadows._Activity_;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;

/**
 * ActivityController provides low-level APIs to control activity's lifecycle.
 *
 * 

Using ActivityController directly from your tests is strongly discouraged. You have to call * all the lifecycle callback methods (create, postCreate, start, ...) in the same manner as the * Android framework by yourself otherwise you'll see fidelity issues. Consider using {@link * androidx.test.core.app.ActivityScenario} instead, which provides higher-level, streamlined APIs * to control the lifecycle and it works with instrumentation tests too. * * @param a class of the activity which is under control by this class. */ @SuppressWarnings("NewApi") public class ActivityController extends ComponentController, T> { private _Activity_ _component_; public static ActivityController of(T activity, Intent intent) { return new ActivityController<>(activity, intent) .attach(/*lastNonConfigurationInstances=*/ null); } public static ActivityController of(T activity) { return new ActivityController<>(activity, null).attach(/*lastNonConfigurationInstances=*/ null); } private ActivityController(T activity, Intent intent) { super(activity, intent); _component_ = reflector(_Activity_.class, component); } private ActivityController attach( @Nullable @WithType("android.app.Activity$NonConfigurationInstances") Object lastNonConfigurationInstances) { if (attached) { return this; } // make sure the component is enabled Context context = RuntimeEnvironment.application.getBaseContext(); PackageManager packageManager = context.getPackageManager(); ComponentName componentName = new ComponentName(context.getPackageName(), this.component.getClass().getName()); ((ShadowPackageManager) extract(packageManager)).addActivityIfNotPresent(componentName); packageManager .setComponentEnabledSetting( componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); ShadowActivity shadowActivity = Shadow.extract(component); shadowActivity.callAttach(getIntent(), lastNonConfigurationInstances); shadowActivity.attachController(this); attached = true; return this; } private ActivityInfo getActivityInfo(Application application) { PackageManager packageManager = application.getPackageManager(); ComponentName componentName = new ComponentName(application.getPackageName(), this.component.getClass().getName()); try { return packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } } public ActivityController create(final Bundle bundle) { shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle)); return this; } @Override public ActivityController create() { return create(null); } public ActivityController restart() { if (RuntimeEnvironment.getApiLevel() <= O_MR1) { invokeWhilePaused("performRestart"); } else { invokeWhilePaused("performRestart", from(boolean.class, true), from(String.class, "restart()")); } return this; } public ActivityController start() { // Start and stop are tricky cases. Unlike other lifecycle methods such as // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite. if (RuntimeEnvironment.getApiLevel() <= O_MR1) { invokeWhilePaused("performStart"); } else { invokeWhilePaused("performStart", from(String.class, "start()")); } return this; } public ActivityController restoreInstanceState(Bundle bundle) { shadowMainLooper.runPaused( () -> getInstrumentation().callActivityOnRestoreInstanceState(component, bundle)); return this; } public ActivityController postCreate(Bundle bundle) { invokeWhilePaused("onPostCreate", from(Bundle.class, bundle)); return this; } public ActivityController resume() { if (RuntimeEnvironment.getApiLevel() <= O_MR1) { invokeWhilePaused("performResume"); } else { invokeWhilePaused("performResume", from(boolean.class, true), from(String.class, "resume()")); } return this; } public ActivityController postResume() { invokeWhilePaused("onPostResume"); return this; } public ActivityController visible() { shadowMainLooper.runPaused( () -> { // emulate logic of ActivityThread#handleResumeActivity component.getWindow().getAttributes().type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; _component_.setDecor(component.getWindow().getDecorView()); ReflectionHelpers.callInstanceMethod(component, "makeVisible"); }); shadowMainLooper.idleIfPaused(); ViewRootImpl root = getViewRoot(); // root can be null if activity does not have content attached, or if looper is paused. // this is unusual but leave the check here for legacy compatibility if (root != null) { callDispatchResized(root); shadowMainLooper.idleIfPaused(); } return this; } private ViewRootImpl getViewRoot() { return component.getWindow().getDecorView().getViewRootImpl(); } private void callDispatchResized(ViewRootImpl root) { ((ShadowViewRootImpl) extract(root)).callDispatchResized(); } public ActivityController windowFocusChanged(boolean hasFocus) { ViewRootImpl root = getViewRoot(); if (root == null) { // root can be null if looper was paused during visible. Flush the looper and try again shadowMainLooper.idle(); root = checkNotNull(getViewRoot()); callDispatchResized(root); } ReflectionHelpers.callInstanceMethod(root, "windowFocusChanged", from(boolean.class, hasFocus), /* hasFocus */ from(boolean.class, false) /* inTouchMode */); shadowMainLooper.idleIfPaused(); return this; } public ActivityController userLeaving() { shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnUserLeaving(component)); return this; } public ActivityController pause() { shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component)); return this; } public ActivityController saveInstanceState(Bundle outState) { shadowMainLooper.runPaused( () -> getInstrumentation().callActivityOnSaveInstanceState(component, outState)); return this; } public ActivityController stop() { // Stop and start are tricky cases. Unlike other lifecycle methods such as // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite. if (RuntimeEnvironment.getApiLevel() <= M) { invokeWhilePaused("performStop"); } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) { invokeWhilePaused("performStop", from(boolean.class, true)); } else { invokeWhilePaused("performStop", from(boolean.class, true), from(String.class, "stop()")); } return this; } @Override public ActivityController destroy() { shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnDestroy(component)); return this; } /** * Calls the same lifecycle methods on the Activity called by Android the first time the Activity is created. * * @return Activity controller instance. */ public ActivityController setup() { return create().start().postCreate(null).resume().visible(); } /** * Calls the same lifecycle methods on the Activity called by Android when an Activity is restored from previously saved state. * * @param savedInstanceState Saved instance state. * @return Activity controller instance. */ public ActivityController setup(Bundle savedInstanceState) { return create(savedInstanceState) .start() .restoreInstanceState(savedInstanceState) .postCreate(savedInstanceState) .resume() .visible(); } public ActivityController newIntent(Intent intent) { invokeWhilePaused("onNewIntent", from(Intent.class, intent)); return this; } /** * Applies the current system configuration to the Activity. * * This can be used in conjunction with {@link RuntimeEnvironment#setQualifiers(String)} to * simulate configuration changes. * * If the activity is configured to handle changes without being recreated, * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html). * * @return ActivityController instance */ public ActivityController configurationChange() { return configurationChange(component.getApplicationContext().getResources().getConfiguration()); } /** * Performs a configuration change on the Activity. * * If the activity is configured to handle changes without being recreated, * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html). * * @param newConfiguration The new configuration to be set. * @return ActivityController instance */ public ActivityController configurationChange(final Configuration newConfiguration) { final Configuration currentConfig = component.getResources().getConfiguration(); final int changedBits = currentConfig.diff(newConfiguration); currentConfig.setTo(newConfiguration); // TODO: throw on changedBits == 0 since it non-intuitively calls onConfigurationChanged // Can the activity handle itself ALL configuration changes? if ((getActivityInfo(component.getApplication()).configChanges & changedBits) == changedBits) { shadowMainLooper.runPaused(() -> component.onConfigurationChanged(newConfiguration)); return this; } else { @SuppressWarnings("unchecked") final T recreatedActivity = (T) ReflectionHelpers.callConstructor(component.getClass()); final _Activity_ _recreatedActivity_ = reflector(_Activity_.class, recreatedActivity); shadowMainLooper.runPaused( new Runnable() { @Override public void run() { // Set flags ReflectionHelpers.setField( Activity.class, component, "mChangingConfigurations", true); ReflectionHelpers.setField( Activity.class, component, "mConfigChangeFlags", changedBits); // Perform activity destruction final Bundle outState = new Bundle(); // The order of onPause/onStop/onSaveInstanceState is undefined, but is usually: // onPause -> onSaveInstanceState -> onStop _component_.performPause(); _component_.performSaveInstanceState(outState); if (RuntimeEnvironment.getApiLevel() <= M) { _component_.performStop(); } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) { _component_.performStop(true); } else { _component_.performStop(true, "configurationChange"); } // This is the true and complete retained state, including loaders and retained // fragments. final Object nonConfigInstance = _component_.retainNonConfigurationInstances(); // This is the activity's "user" state final Object activityConfigInstance = nonConfigInstance == null ? null // No framework or user state. : reflector(_NonConfigurationInstances_.class, nonConfigInstance) .getActivity(); _component_.performDestroy(); // Restore theme in case it was set in the test manually. // This is not technically what happens but is purely to make this easier to use in // Robolectric. ShadowContextThemeWrapper shadowContextThemeWrapper = Shadow.extract(component); int theme = shadowContextThemeWrapper.callGetThemeResId(); // Setup controller for the new activity attached = false; component = recreatedActivity; _component_ = _recreatedActivity_; // TODO: Pass nonConfigurationInstance here instead of setting // mLastNonConfigurationInstances directly below. This field must be set before // attach. Since current implementation sets it after attach(), initialization is not // done correctly. For instance, fragment marked as retained is not retained. attach(/*lastNonConfigurationInstances=*/ null); if (theme != 0) { recreatedActivity.setTheme(theme); } // Set saved non config instance _recreatedActivity_.setLastNonConfigurationInstances(nonConfigInstance); ShadowActivity shadowActivity = Shadow.extract(recreatedActivity); shadowActivity.setLastNonConfigurationInstance(activityConfigInstance); // Create lifecycle _recreatedActivity_.performCreate(outState); if (RuntimeEnvironment.getApiLevel() <= O_MR1) { _recreatedActivity_.performStart(); } else { _recreatedActivity_.performStart("configurationChange"); } _recreatedActivity_.performRestoreInstanceState(outState); _recreatedActivity_.onPostCreate(outState); if (RuntimeEnvironment.getApiLevel() <= O_MR1) { _recreatedActivity_.performResume(); } else { _recreatedActivity_.performResume(true, "configurationChange"); } _recreatedActivity_.onPostResume(); // TODO: Call visible() too. } }); } return this; } /** * Recreates activity instance which is controlled by this ActivityController. * NonConfigurationInstances and savedInstanceStateBundle are properly passed into a new instance. * After the recreation, it brings back its lifecycle state to the original state. The activity * should not be destroyed yet. */ @SuppressWarnings("unchecked") public ActivityController recreate() { Stage originalStage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(component); switch (originalStage) { case PRE_ON_CREATE: create(); // fall through case CREATED: case RESTARTED: start(); postCreate(null); // fall through case STARTED: resume(); // fall through case RESUMED: pause(); // fall through case PAUSED: stop(); // fall through case STOPPED: break; default: throw new IllegalStateException("Cannot recreate activity since it's destroyed already"); } // Activity#mChangingConfigurations flag should be set prior to Activity recreation process // starts. ActivityThread does set it on real device but here we simulate the Activity // recreation process on behalf of ActivityThread so set the flag here. Note we don't need to // reset the flag to false because this Activity instance is going to be destroyed and disposed. // https://android.googlesource.com/platform/frameworks/base/+/55418eada51d4f5e6532ae9517af66c50 // ea495c4/core/java/android/app/ActivityThread.java#4806 ReflectionHelpers.setField(component, "mChangingConfigurations", true); Bundle outState = new Bundle(); saveInstanceState(outState); Object lastNonConfigurationInstances = ReflectionHelpers.callInstanceMethod(component, "retainNonConfigurationInstances"); destroy(); component = (T) ReflectionHelpers.callConstructor(component.getClass()); _component_ = reflector(_Activity_.class, component); attached = false; attach(lastNonConfigurationInstances); create(outState); start(); restoreInstanceState(outState); postCreate(outState); resume(); postResume(); visible(); windowFocusChanged(true); // Move back to the original stage. If the original stage was transient stage, it will bring it // to resumed state to match the on device behavior. switch (originalStage) { case PAUSED: pause(); return this; case STOPPED: pause(); stop(); return this; default: return this; } } private static Instrumentation getInstrumentation() { return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation(); } /** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */ @ForType(className = "android.app.Activity$NonConfigurationInstances") interface _NonConfigurationInstances_ { @Accessor("activity") Object getActivity(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy