blob: 87db0379defadadfdda2b2ac94e1f83284414621 [file] [log] [blame]
package org.robolectric.util;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.ShadowsAdapter;
import org.robolectric.ShadowsAdapter.ShadowActivityAdapter;
import org.robolectric.ShadowsAdapter.ShadowApplicationAdapter;
import org.robolectric.internal.Shadow;
import org.robolectric.internal.runtime.RuntimeAdapter;
import org.robolectric.internal.runtime.RuntimeAdapterFactory;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import android.app.Activity;
import android.app.Application;
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.graphics.Rect;
import android.os.Bundle;
import android.view.Display;
import android.view.ViewRootImpl;
public class ActivityController<T extends Activity> extends ComponentController<ActivityController<T>, T> {
private final ShadowsAdapter shadowsAdapter;
private ShadowActivityAdapter shadowReference;
public static <T extends Activity> ActivityController<T> of(ShadowsAdapter shadowsAdapter, T activity, Intent intent) {
return new ActivityController<>(shadowsAdapter, activity, intent).attach();
}
public static <T extends Activity> ActivityController<T> of(ShadowsAdapter shadowsAdapter, T activity) {
return new ActivityController<>(shadowsAdapter, activity, null).attach();
}
private ActivityController(ShadowsAdapter shadowsAdapter, T activity, Intent intent) {
super(shadowsAdapter, activity, intent);
this.shadowsAdapter = shadowsAdapter;
shadowReference = shadowsAdapter.getShadowActivityAdapter(this.component);
}
public ActivityController<T> withIntent(Intent intent) {
super.withIntent(intent);
// This is a hack to support existing usages where withIntent() is called after attach().
ReflectionHelpers.setField(component, "mIntent", getIntent());
ReflectionHelpers.setField(component, "mComponent", getIntent().getComponent());
return myself;
}
public ActivityController<T> attach() {
if (attached) {
return this;
}
Context baseContext = RuntimeEnvironment.application.getBaseContext();
final String title = getActivityTitle();
final ClassLoader cl = baseContext.getClassLoader();
final ActivityInfo info = getActivityInfo(RuntimeEnvironment.application);
final Class<?> threadClass = getActivityThreadClass(cl);
final Class<?> nonConfigurationClass = getNonConfigurationClass(cl);
final RuntimeAdapter runtimeAdapter = RuntimeAdapterFactory.getInstance();
runtimeAdapter.callActivityAttach(component, baseContext, threadClass, RuntimeEnvironment.application, getIntent(), info, title, nonConfigurationClass);
shadowReference.setThemeFromManifest();
attached = true;
return this;
}
private ActivityInfo getActivityInfo(Application application) {
try {
return application.getPackageManager().getActivityInfo(new ComponentName(application.getPackageName(), component.getClass().getName()), PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
private Class<?> getActivityThreadClass(ClassLoader cl) {
try {
return cl.loadClass(shadowsAdapter.getShadowActivityThreadClassName());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private Class<?> getNonConfigurationClass(ClassLoader cl) {
try {
return cl.loadClass("android.app.Activity$NonConfigurationInstances");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private String getActivityTitle() {
String title = null;
/* Get the label for the activity from the manifest */
ShadowApplicationAdapter shadowApplicationAdapter = shadowsAdapter.getApplicationAdapter(component);
AndroidManifest appManifest = shadowApplicationAdapter.getAppManifest();
if (appManifest == null) return null;
String labelRef = appManifest.getActivityLabel(component.getClass());
if (labelRef != null) {
if (labelRef.startsWith("@")) {
/* Label refers to a string value, get the resource identifier */
int labelRes = RuntimeEnvironment.application.getResources().getIdentifier(labelRef.replace("@", ""), "string", appManifest.getPackageName());
/* Get the resource ID, use the activity to look up the actual string */
title = RuntimeEnvironment.application.getString(labelRes);
} else {
title = labelRef; /* Label isn't an identifier, use it directly as the title */
}
}
return title;
}
public ActivityController<T> create(final Bundle bundle) {
shadowMainLooper.runPaused(new Runnable() {
@Override
public void run() {
ReflectionHelpers.callInstanceMethod(Activity.class, component, "performCreate", ClassParameter.from(Bundle.class, bundle));
}
});
return this;
}
public ActivityController<T> create() {
return create(null);
}
public ActivityController<T> restoreInstanceState(Bundle bundle) {
invokeWhilePaused("performRestoreInstanceState", bundle);
return this;
}
public ActivityController<T> postCreate(Bundle bundle) {
invokeWhilePaused("onPostCreate", bundle);
return this;
}
public ActivityController<T> start() {
invokeWhilePaused("performStart");
return this;
}
public ActivityController<T> restart() {
invokeWhilePaused("performRestart");
return this;
}
public ActivityController<T> resume() {
invokeWhilePaused("performResume");
return this;
}
public ActivityController<T> postResume() {
invokeWhilePaused("onPostResume");
return this;
}
public ActivityController<T> newIntent(Intent intent) {
invokeWhilePaused("onNewIntent", intent);
return this;
}
public ActivityController<T> saveInstanceState(Bundle outState) {
invokeWhilePaused("performSaveInstanceState", outState);
return this;
}
public ActivityController<T> visible() {
shadowMainLooper.runPaused(new Runnable() {
@Override
public void run() {
ReflectionHelpers.setField(component, "mDecor", component.getWindow().getDecorView());
ReflectionHelpers.callInstanceMethod(component, "makeVisible");
}
});
ViewRootImpl root = component.getWindow().getDecorView().getViewRootImpl();
if (root != null) {
// If a test pause thread before creating an activity, root will be null as runPaused is waiting
// Related to issue #1582
Display display = Shadow.newInstanceOf(Display.class);
Rect frame = new Rect();
display.getRectSize(frame);
Rect insets = new Rect(0, 0, 0, 0);
final RuntimeAdapter runtimeAdapter = RuntimeAdapterFactory.getInstance();
runtimeAdapter.callViewRootImplDispatchResized(
root, frame, insets, insets, insets, insets, insets, true, null);
}
return this;
}
public ActivityController<T> pause() {
invokeWhilePaused("performPause");
return this;
}
public ActivityController<T> userLeaving() {
invokeWhilePaused("performUserLeaving");
return this;
}
public ActivityController<T> stop() {
invokeWhilePaused("performStop");
return this;
}
public ActivityController<T> destroy() {
invokeWhilePaused("performDestroy");
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<T> 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<T> setup(Bundle savedInstanceState) {
return create(savedInstanceState)
.start()
.restoreInstanceState(savedInstanceState)
.postCreate(savedInstanceState)
.resume()
.visible();
}
/**
* Performs a configuration change on the Activity.
*
* @param newConfiguration The new configuration to be set.
* @return Activity controller instance.
*/
public ActivityController<T> configurationChange(final Configuration newConfiguration) {
final Configuration currentConfig = component.getResources().getConfiguration();
final int changedBits = currentConfig.diff(newConfiguration);
currentConfig.setTo(newConfiguration);
// Can the activity handle itself ALL configuration changes?
if ((getActivityInfo(component.getApplication()).configChanges & changedBits) == changedBits) {
shadowMainLooper.runPaused(new Runnable() {
@Override
public void run() {
ReflectionHelpers.callInstanceMethod(Activity.class, component, "onConfigurationChanged",
ClassParameter.from(Configuration.class, newConfiguration));
}
});
return this;
} else {
@SuppressWarnings("unchecked")
final T recreatedActivity = (T) ReflectionHelpers.callConstructor(component.getClass());
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();
ReflectionHelpers.callInstanceMethod(Activity.class, component, "onSaveInstanceState",
ClassParameter.from(Bundle.class, outState));
ReflectionHelpers.callInstanceMethod(Activity.class, component, "onPause");
ReflectionHelpers.callInstanceMethod(Activity.class, component, "onStop");
final Object nonConfigInstance = ReflectionHelpers.callInstanceMethod(
Activity.class, component, "onRetainNonConfigurationInstance");
ReflectionHelpers.callInstanceMethod(Activity.class, component, "onDestroy");
// Setup controller for the new activity
attached = false;
component = recreatedActivity;
shadowReference = shadowsAdapter.getShadowActivityAdapter(component);
attach();
// Set saved non config instance
Shadows.shadowOf(recreatedActivity).setLastNonConfigurationInstance(nonConfigInstance);
// Create lifecycle
ReflectionHelpers.callInstanceMethod(Activity.class, recreatedActivity,
"onCreate", ClassParameter.from(Bundle.class, outState));
ReflectionHelpers.callInstanceMethod(Activity.class, recreatedActivity, "onStart");
ReflectionHelpers.callInstanceMethod(Activity.class, recreatedActivity,
"onRestoreInstanceState", ClassParameter.from(Bundle.class, outState));
ReflectionHelpers.callInstanceMethod(Activity.class, recreatedActivity,
"onPostCreate", ClassParameter.from(Bundle.class, outState));
ReflectionHelpers.callInstanceMethod(Activity.class, recreatedActivity, "onResume");
}
});
}
return this;
}
}