Skip to content

Commit

Permalink
Add ActivityController.close that transitions Activity to destroyed s…
Browse files Browse the repository at this point in the history
…tate

Update ActivityController to implement AutoCloseable, and add a 'close' method
that is lifecycle-aware and transitions the underlying Activity to the destroyed
state, freeing all resources and making the Activity eligible for gc.

This is a convenient way to ensure that ActivityControllers can be freed without
having to manage the underlying Activity lifecycles.

It also enables ActivityController to be managed using try-with-resources.

PiperOrigin-RevId: 410291892
  • Loading branch information
hoisie committed Nov 17, 2021
1 parent 326991f commit 31be693
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 35 deletions.
Expand Up @@ -379,6 +379,77 @@ public void windowFocusChanged() {
assertThat(controller.get().hasWindowFocus()).isTrue();
}

@Test
public void close_transitionsActivityStateToDestroyed() {
Robolectric.buildActivity(MyActivity.class).close();
assertThat(transcript).isEmpty();
transcript.clear();

Robolectric.buildActivity(MyActivity.class).create().close();
assertThat(transcript)
.containsExactly("onCreate", "finishedOnCreate", "onDestroy", "finishedOnDestroy");
transcript.clear();

Robolectric.buildActivity(MyActivity.class).create().start().close();
assertThat(transcript)
.containsExactly(
"onCreate",
"finishedOnCreate",
"onStart",
"finishedOnStart",
"onStop",
"finishedOnStop",
"onDestroy",
"finishedOnDestroy");
transcript.clear();

Robolectric.buildActivity(MyActivity.class).setup().close();
assertThat(transcript)
.containsExactly(
"onCreate",
"finishedOnCreate",
"onStart",
"finishedOnStart",
"onPostCreate",
"finishedOnPostCreate",
"onResume",
"finishedOnResume",
"onPostResume",
"finishedOnPostResume",
"onPause",
"finishedOnPause",
"onStop",
"finishedOnStop",
"onDestroy",
"finishedOnDestroy");
}

@Test
public void close_tryWithResources_getsDestroyed() {
try (ActivityController<MyActivity> ignored =
Robolectric.buildActivity(MyActivity.class).setup()) {
// no-op
}
assertThat(transcript)
.containsExactly(
"onCreate",
"finishedOnCreate",
"onStart",
"finishedOnStart",
"onPostCreate",
"finishedOnPostCreate",
"onResume",
"finishedOnResume",
"onPostResume",
"finishedOnPostResume",
"onPause",
"finishedOnPause",
"onStop",
"finishedOnStop",
"onDestroy",
"finishedOnDestroy");
}

public static class MyActivity extends Activity {
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
Expand Down
Expand Up @@ -7,7 +7,6 @@
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;
Expand All @@ -19,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 All @@ -47,9 +44,21 @@
*/
@SuppressWarnings("NewApi")
public class ActivityController<T extends Activity>
extends ComponentController<ActivityController<T>, T> {
extends ComponentController<ActivityController<T>, T> implements AutoCloseable {

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 @@ -110,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 @@ -119,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 @@ -151,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 @@ -215,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 @@ -229,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 @@ -245,6 +278,7 @@ public ActivityController<T> destroy() {
() -> {
getInstrumentation().callActivityOnDestroy(component);
makeActivityEligibleForGc();
currentState = LifecycleState.DESTROYED;
});
return this;
}
Expand Down Expand Up @@ -434,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 @@ -489,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 All @@ -502,8 +536,41 @@ public ActivityController<T> recreate() {
}
}

private static Instrumentation getInstrumentation() {
return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation();
// Get the Instrumentation object scoped to the Activity.
private Instrumentation getInstrumentation() {
return _component_.getInstrumentation();
}

/**
* Transitions the underlying Activity to the 'destroyed' state by progressing through the
* appropriate lifecycle events. It frees up any resources and makes the Activity eligible for GC.
*/
@Override
public void close() {

LifecycleState originalState = currentState;

switch (originalState) {
case INITIAL:
case DESTROYED:
return;
case RESUMED:
pause();
// fall through
case PAUSED:
// fall through
case RESTARTED:
// fall through
case STARTED:
stop();
// fall through
case STOPPED:
// fall through
case CREATED:
break;
}

destroy();
}

/** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */
Expand Down

0 comments on commit 31be693

Please sign in to comment.