Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix UnsupportedOperationException if a leaked Activity.recreate is called #6862

Merged
merged 1 commit into from Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,34 @@
package org.robolectric.android.controller;

import android.app.Activity;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;

/**
* This test captures an issue where {@link ActivityController#recreate()} would throw an {@link
* UnsupportedOperationException} if an Activity from a previous test was recreated.
*/
@RunWith(AndroidJUnit4.class)
public class ActivityControllerRecreateTest {
private static final AtomicReference<ActivityController<Activity>> createdActivity =
new AtomicReference<>();

@Before
public void setUp() {
createdActivity.compareAndSet(null, Robolectric.buildActivity(Activity.class).create());
}

@Test
public void failsTryingToRecreateActivityFromOtherTest1() {
createdActivity.get().recreate();
}

@Test
public void failsTryingToRecreateActivityFromOtherTest2() {
createdActivity.get().recreate();
}
}
Expand Up @@ -18,8 +18,6 @@
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;
Expand Down Expand Up @@ -48,7 +46,19 @@
public class ActivityController<T extends Activity>
extends ComponentController<ActivityController<T>, T> {

enum LifecycleState {
INITIAL,
CREATED,
RESTARTED,
STARTED,
RESUMED,
PAUSED,
STOPPED,
DESTROYED
}

private _Activity_ _component_;
private LifecycleState currentState = LifecycleState.INITIAL;

public static <T extends Activity> ActivityController<T> of(
T activity, Intent intent, @Nullable Bundle activityOptions) {
Expand Down Expand Up @@ -109,7 +119,11 @@ private ActivityInfo getActivityInfo(Application application) {
}

public ActivityController<T> create(@Nullable final Bundle bundle) {
shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle));
shadowMainLooper.runPaused(
() -> {
getInstrumentation().callActivityOnCreate(component, bundle);
currentState = LifecycleState.CREATED;
});
return this;
}

Expand All @@ -118,23 +132,31 @@ public ActivityController<T> create(@Nullable final Bundle bundle) {
}

public ActivityController<T> restart() {
if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
invokeWhilePaused(() -> _component_.performRestart());
} else {
invokeWhilePaused(() -> _component_.performRestart(true, "restart()"));
}
invokeWhilePaused(
() -> {
if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
_component_.performRestart();
} else {
_component_.performRestart(true, "restart()");
}
currentState = LifecycleState.RESTARTED;
});
return this;
}

public ActivityController<T> 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(() -> _component_.performStart());
} else {
invokeWhilePaused(() -> _component_.performStart("start()"));
}
invokeWhilePaused(
() -> {
if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
_component_.performStart();
} else {
_component_.performStart("start()");
}
currentState = LifecycleState.STARTED;
});
return this;
}

Expand All @@ -150,11 +172,15 @@ public ActivityController<T> postCreate(@Nullable Bundle bundle) {
}

public ActivityController<T> resume() {
if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
invokeWhilePaused(() -> _component_.performResume());
} else {
invokeWhilePaused(() -> _component_.performResume(true, "resume()"));
}
invokeWhilePaused(
() -> {
if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
_component_.performResume();
} else {
_component_.performResume(true, "resume()");
}
currentState = LifecycleState.RESUMED;
});
return this;
}

Expand Down Expand Up @@ -214,7 +240,11 @@ public ActivityController<T> userLeaving() {
}

public ActivityController<T> pause() {
shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component));
shadowMainLooper.runPaused(
() -> {
getInstrumentation().callActivityOnPause(component);
currentState = LifecycleState.PAUSED;
});
return this;
}

Expand All @@ -228,13 +258,17 @@ public ActivityController<T> 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(() -> _component_.performStop());
} else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
invokeWhilePaused(() -> _component_.performStop(true));
} else {
invokeWhilePaused(() -> _component_.performStop(true, "stop()"));
}
invokeWhilePaused(
() -> {
if (RuntimeEnvironment.getApiLevel() <= M) {
_component_.performStop();
} else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
_component_.performStop(true);
} else {
_component_.performStop(true, "stop()");
}
currentState = LifecycleState.STOPPED;
});
return this;
}

Expand All @@ -244,6 +278,7 @@ public ActivityController<T> destroy() {
() -> {
getInstrumentation().callActivityOnDestroy(component);
makeActivityEligibleForGc();
currentState = LifecycleState.DESTROYED;
});
return this;
}
Expand Down Expand Up @@ -433,11 +468,11 @@ public ActivityController<T> configurationChange(final Configuration newConfigur
*/
@SuppressWarnings("unchecked")
public ActivityController<T> recreate() {
Stage originalStage =
ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(component);

switch (originalStage) {
case PRE_ON_CREATE:
LifecycleState originalState = currentState;

switch (originalState) {
case INITIAL:
create();
// fall through
case CREATED:
Expand Down Expand Up @@ -488,7 +523,7 @@ public ActivityController<T> recreate() {

// 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) {
switch (originalState) {
case PAUSED:
pause();
return this;
Expand Down