blob: 289d9c038caaadeb72b6e657919283600b5fbe37 [file] [log] [blame]
package org.robolectric.android.internal;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import android.app.Activity;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
import android.util.DisplayMetrics;
import android.util.Log;
import androidx.test.internal.runner.intent.IntentMonitorImpl;
import androidx.test.internal.runner.lifecycle.ActivityLifecycleMonitorImpl;
import androidx.test.internal.runner.lifecycle.ApplicationLifecycleMonitorImpl;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.intent.IntentMonitorRegistry;
import androidx.test.runner.intent.IntentStubberRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.ApplicationLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.ApplicationStage;
import androidx.test.runner.lifecycle.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.shadows.ShadowActivity;
/**
* A Robolectric instrumentation that acts like a slimmed down {@link
* androidx.test.runner.MonitoringInstrumentation} with only the parts needed for Robolectric.
*/
@SuppressWarnings("RestrictTo")
public class RoboMonitoringInstrumentation extends Instrumentation {
private static final String TAG = "RoboInstrumentation";
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
private final ActivityLifecycleMonitorImpl lifecycleMonitor = new ActivityLifecycleMonitorImpl();
private final ApplicationLifecycleMonitorImpl applicationMonitor =
new ApplicationLifecycleMonitorImpl();
private final IntentMonitorImpl intentMonitor = new IntentMonitorImpl();
private final List<ActivityController<?>> createdActivities = new ArrayList<>();
/**
* Sets up lifecycle monitoring, and argument registry.
*
* <p>Subclasses must call up to onCreate(). This onCreate method does not call start() it is the
* subclasses responsibility to call start if it desires.
*/
@Override
public void onCreate(Bundle arguments) {
InstrumentationRegistry.registerInstance(this, arguments);
ActivityLifecycleMonitorRegistry.registerInstance(lifecycleMonitor);
ApplicationLifecycleMonitorRegistry.registerInstance(applicationMonitor);
IntentMonitorRegistry.registerInstance(intentMonitor);
shadowOf(Resources.getSystem()).addConfigurationChangeListener(this::updateConfiguration);
super.onCreate(arguments);
}
@Override
public void waitForIdleSync() {
shadowMainLooper().idle();
}
@Override
public Activity startActivitySync(final Intent intent) {
return startActivitySyncInternal(intent).get();
}
public ActivityController<? extends Activity> startActivitySyncInternal(Intent intent) {
return startActivitySyncInternal(intent, /* activityOptions= */ null);
}
public ActivityController<? extends Activity> startActivitySyncInternal(
Intent intent, @Nullable Bundle activityOptions) {
ActivityInfo ai = intent.resolveActivityInfo(getTargetContext().getPackageManager(), 0);
if (ai == null) {
throw new RuntimeException("Unable to resolve activity for " + intent
+ " -- see https://github.com/robolectric/robolectric/pull/4736 for details");
}
Class<? extends Activity> activityClass;
String activityClassName = ai.targetActivity != null ? ai.targetActivity : ai.name;
try {
activityClass = Class.forName(activityClassName).asSubclass(Activity.class);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Could not load activity " + ai.name, e);
}
ActivityController<? extends Activity> controller =
Robolectric.buildActivity(activityClass, intent, activityOptions).create();
if (controller.get().isFinishing()) {
controller.destroy();
} else {
createdActivities.add(controller);
controller.start()
.postCreate(null)
.resume()
.visible()
.windowFocusChanged(true);
}
return controller;
}
@Override
public void callApplicationOnCreate(Application app) {
applicationMonitor.signalLifecycleChange(app, ApplicationStage.PRE_ON_CREATE);
super.callApplicationOnCreate(app);
applicationMonitor.signalLifecycleChange(app, ApplicationStage.CREATED);
}
@Override
public void runOnMainSync(Runnable runner) {
shadowMainLooper().idle();
runner.run();
}
/** {@inheritDoc} */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
Activity target,
Intent intent,
int requestCode,
Bundle options) {
intentMonitor.signalIntent(intent);
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
} else {
ar = super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
if (ar != null && target != null) {
ShadowActivity shadowActivity = extract(target);
postDispatchActivityResult(shadowActivity, null, requestCode, ar);
}
return null;
}
/** This API was added in Android API 23 (M) */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
String target,
Intent intent,
int requestCode,
Bundle options) {
intentMonitor.signalIntent(intent);
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
} else {
ar = super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
if (ar != null && who instanceof Activity) {
ShadowActivity shadowActivity = extract(who);
postDispatchActivityResult(shadowActivity, target, requestCode, ar);
}
return null;
}
/** This API was added in Android API 17 (JELLY_BEAN_MR1) */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
String target,
Intent intent,
int requestCode,
Bundle options,
UserHandle user) {
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
} else {
ar =
super.execStartActivity(
who, contextThread, token, target, intent, requestCode, options, user);
}
if (ar != null && target != null) {
ShadowActivity shadowActivity = extract(target);
postDispatchActivityResult(shadowActivity, null, requestCode, ar);
}
return null;
}
private void postDispatchActivityResult(
ShadowActivity shadowActivity, String target, int requestCode, ActivityResult ar) {
mainThreadHandler.post(
new Runnable() {
@Override
public void run() {
shadowActivity.internalCallDispatchActivityResult(
target, requestCode, ar.getResultCode(), ar.getResultData());
}
});
}
private ActivityResult stubResultFor(Intent intent) {
if (IntentStubberRegistry.isLoaded()) {
return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent);
}
return null;
}
/** {@inheritDoc} */
@Override
public void execStartActivities(
Context who,
IBinder contextThread,
IBinder token,
Activity target,
Intent[] intents,
Bundle options) {
Log.d(TAG, "execStartActivities(context, ibinder, ibinder, activity, intent[], bundle)");
// For requestCode < 0, the caller doesn't expect any result and
// in this case we are not expecting any result so selecting
// a value < 0.
int requestCode = -1;
for (Intent intent : intents) {
execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
}
@Override
public boolean onException(Object obj, Throwable e) {
String error =
String.format(
"Exception encountered by: %s. Dumping thread state to "
+ "outputs and pining for the fjords.",
obj);
Log.e(TAG, error, e);
Log.e("THREAD_STATE", getThreadState());
Log.e(TAG, "Dying now...");
return super.onException(obj, e);
}
protected String getThreadState() {
Set<Map.Entry<Thread, StackTraceElement[]>> threads = Thread.getAllStackTraces().entrySet();
StringBuilder threadState = new StringBuilder();
for (Map.Entry<Thread, StackTraceElement[]> threadAndStack : threads) {
StringBuilder threadMessage = new StringBuilder(" ").append(threadAndStack.getKey());
threadMessage.append("\n");
for (StackTraceElement ste : threadAndStack.getValue()) {
threadMessage.append(String.format(" %s%n", ste));
}
threadMessage.append("\n");
threadState.append(threadMessage);
}
return threadState.toString();
}
@Override
public void callActivityOnDestroy(Activity activity) {
if (activity.isFinishing()) {
createdActivities.removeIf(controller -> controller.get() == activity);
}
super.callActivityOnDestroy(activity);
lifecycleMonitor.signalLifecycleChange(Stage.DESTROYED, activity);
}
@Override
public void callActivityOnRestart(Activity activity) {
super.callActivityOnRestart(activity);
lifecycleMonitor.signalLifecycleChange(Stage.RESTARTED, activity);
}
@Override
public void callActivityOnCreate(Activity activity, Bundle bundle) {
lifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity);
super.callActivityOnCreate(activity, bundle);
lifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity);
}
@Override
public void callActivityOnStart(Activity activity) {
super.callActivityOnStart(activity);
lifecycleMonitor.signalLifecycleChange(Stage.STARTED, activity);
}
@Override
public void callActivityOnStop(Activity activity) {
super.callActivityOnStop(activity);
lifecycleMonitor.signalLifecycleChange(Stage.STOPPED, activity);
}
@Override
public void callActivityOnResume(Activity activity) {
super.callActivityOnResume(activity);
lifecycleMonitor.signalLifecycleChange(Stage.RESUMED, activity);
}
@Override
public void callActivityOnPause(Activity activity) {
super.callActivityOnPause(activity);
lifecycleMonitor.signalLifecycleChange(Stage.PAUSED, activity);
}
@Override
public void finish(int resultCode, Bundle bundle) { }
@Override
public Context getTargetContext() {
return RuntimeEnvironment.getApplication();
}
@Override
public Context getContext() {
return RuntimeEnvironment.getApplication();
}
private void updateConfiguration(
Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics) {
int changedConfig = oldConfig.diff(newConfig);
List<ActivityController<?>> controllers = new ArrayList<>(createdActivities);
for (ActivityController<?> controller : controllers) {
if (createdActivities.contains(controller)) {
Activity activity = controller.get();
controller.configurationChange(newConfig, newMetrics, changedConfig);
// If the activity is recreated then make the new activity visible, this should be done by
// configurationChange but there's a pre-existing TODO to address this and it will require
// more work to make it function correctly.
if (controller.get() != activity) {
controller.visible();
}
}
}
}
}