From 31be693e889a86a413661bfed638ec013e88bec3 Mon Sep 17 00:00:00 2001 From: hoisie Date: Tue, 16 Nov 2021 10:36:20 -0800 Subject: [PATCH] Add ActivityController.close that transitions Activity to destroyed state 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 --- .../controller/ActivityControllerTest.java | 71 +++++++++ .../controller/ActivityController.java | 137 +++++++++++++----- 2 files changed, 173 insertions(+), 35 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java index 23c563a833b..1a3ef964006 100644 --- a/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java +++ b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java @@ -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 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) { diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java index 1105f5d39f3..e5b103bd32e 100644 --- a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java +++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java @@ -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; @@ -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; @@ -47,9 +44,21 @@ */ @SuppressWarnings("NewApi") public class ActivityController - extends ComponentController, T> { + extends ComponentController, T> implements AutoCloseable { + + enum LifecycleState { + INITIAL, + CREATED, + RESTARTED, + STARTED, + RESUMED, + PAUSED, + STOPPED, + DESTROYED + } private _Activity_ _component_; + private LifecycleState currentState = LifecycleState.INITIAL; public static ActivityController of( T activity, Intent intent, @Nullable Bundle activityOptions) { @@ -110,7 +119,11 @@ private ActivityInfo getActivityInfo(Application application) { } public ActivityController create(@Nullable final Bundle bundle) { - shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle)); + shadowMainLooper.runPaused( + () -> { + getInstrumentation().callActivityOnCreate(component, bundle); + currentState = LifecycleState.CREATED; + }); return this; } @@ -119,11 +132,15 @@ public ActivityController create(@Nullable final Bundle bundle) { } public ActivityController 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; } @@ -131,11 +148,15 @@ public ActivityController 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; } @@ -151,11 +172,15 @@ public ActivityController postCreate(@Nullable Bundle bundle) { } public ActivityController 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; } @@ -215,7 +240,11 @@ public ActivityController userLeaving() { } public ActivityController pause() { - shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component)); + shadowMainLooper.runPaused( + () -> { + getInstrumentation().callActivityOnPause(component); + currentState = LifecycleState.PAUSED; + }); return this; } @@ -229,13 +258,17 @@ public ActivityController 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; } @@ -245,6 +278,7 @@ public ActivityController destroy() { () -> { getInstrumentation().callActivityOnDestroy(component); makeActivityEligibleForGc(); + currentState = LifecycleState.DESTROYED; }); return this; } @@ -434,11 +468,11 @@ public ActivityController configurationChange(final Configuration newConfigur */ @SuppressWarnings("unchecked") public ActivityController 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: @@ -489,7 +523,7 @@ public ActivityController 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; @@ -502,8 +536,41 @@ public ActivityController 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. */