Skip to content

Commit

Permalink
Merge pull request #6862 from robolectric/piper_410168815
Browse files Browse the repository at this point in the history
Fix UnsupportedOperationException if a leaked Activity.recreate is called
  • Loading branch information
hoisie committed Nov 17, 2021
2 parents 648e0a1 + 6fdc8ef commit 662a2fe
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 31 deletions.
@@ -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

0 comments on commit 662a2fe

Please sign in to comment.