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 authored and copybara-robolectric committed Nov 17, 2021
1 parent 326991f commit 1be6910
Show file tree
Hide file tree
Showing 21 changed files with 359 additions and 90 deletions.
Expand Up @@ -656,7 +656,7 @@ public boolean isNinePatch() {
// : mStart(0), mLength(0), mOffset(0), mFp(null), mFileName(null), mMap(null), mBuf(null)
{
// Register the Asset with the global list here after it is fully constructed and its
// vtable pointer points to this concrete type. b/31113965
// vtable pointer points to this concrete type.
registerAsset(this);
}

Expand All @@ -668,7 +668,7 @@ protected void finalize() {
close();

// Unregister the Asset from the global list here before it is destructed and while its vtable
// pointer still points to this concrete type. b/31113965
// pointer still points to this concrete type.
unregisterAsset(this);
}

Expand Down Expand Up @@ -1136,7 +1136,7 @@ public boolean isNinePatch() {
mFd = -1;

// Register the Asset with the global list here after it is fully constructed and its
// vtable pointer points to this concrete type. b/31113965
// vtable pointer points to this concrete type.
registerAsset(this);
}

Expand Down Expand Up @@ -1167,7 +1167,7 @@ protected void finalize() {
close();

// Unregister the Asset from the global list here before it is destructed and while its vtable
// pointer still points to this concrete type. b/31113965
// pointer still points to this concrete type.
unregisterAsset(this);
}

Expand Down
Expand Up @@ -166,7 +166,6 @@ Chunk Next() {
return new Chunk(this_chunk);
}

// TODO(b/111401637) remove this and have full resource file verification
// Returns false if there was an error. For legacy purposes.
boolean VerifyNextChunkNonFatal() {
if (len_ < ResChunk_header.SIZEOF) {
Expand Down
Expand Up @@ -51,8 +51,8 @@ enum FileType {
kFileTypeSocket,
}


// transliterated from https://cs.corp.google.com/android/frameworks/base/libs/androidfw/include/androidfw/AssetManager.h
// transliterated from
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/androidfw/include/androidfw/AssetManager.h
private static class asset_path {
// asset_path() : path(""), type(kFileTypeRegular), idmap(""),
// isSystemOverlay(false), isSystemAsset(false) {}
Expand Down
Expand Up @@ -239,7 +239,7 @@ static ImmutableMap<String, Long> guessDataOffsets(File zipFile, int length) {
while (true) {
// Instead of trusting numRecords, read until we find the
// end-of-central-directory signature. numRecords may wrap
// around with >64K entries (b/5455504).
// around with >64K entries.
int sig = readInt(buffer, offset);
if (sig == ENDSIG || sig == ENDSIG64) {
break;
Expand Down
Expand Up @@ -57,7 +57,8 @@ public boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityExceptio
return true;
}

// TODO(b/80130000): implementation copied from espresso's UIControllerImpl. Refactor code into common location
// TODO: implementation copied from espresso's UIControllerImpl. Refactor code into common
// location
@Override
public boolean injectString(String str) throws InjectEventSecurityException {
checkNotNull(str);
Expand All @@ -72,8 +73,7 @@ public boolean injectString(String str) throws InjectEventSecurityException {
boolean eventInjected = false;
KeyCharacterMap keyCharacterMap = getKeyCharacterMap();

// TODO(b/80130875): Investigate why not use (as suggested in javadoc of
// keyCharacterMap.getEvents):
// TODO: Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents):
// http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long,
// java.lang.String, int, int)
KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray());
Expand Down
@@ -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 @@ -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 @@ -4,7 +4,6 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -69,18 +68,16 @@ public void startBugreport_noPermission() throws Exception {
BugreportCallback callback = mock(BugreportCallback.class);
shadowBugreportManager.setHasPermission(false);

// TODO(b/179958637) switch to assertThrows once ThrowingRunnable no longer causes a test
// instantiation failure.
try {
shadowBugreportManager.startBugreport(
createWriteFile("bugreport"),
createWriteFile("screenshot"),
new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL),
directExecutor(),
callback);
fail("Expected SecurityException");
} catch (SecurityException expected) {
}
assertThrows(
SecurityException.class,
() -> {
shadowBugreportManager.startBugreport(
createWriteFile("bugreport"),
createWriteFile("screenshot"),
new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL),
directExecutor(),
callback);
});
shadowMainLooper().idle();

assertThat(shadowBugreportManager.isBugreportInProgress()).isFalse();
Expand Down
Expand Up @@ -2,6 +2,8 @@

import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static com.google.common.truth.Truth.assertThat;

import android.os.Build;
Expand Down Expand Up @@ -56,6 +58,13 @@ public void setVersionIncremental() {
assertThat(VERSION.INCREMENTAL).isEqualTo("robo_incremental");
}

@Test
@Config(minSdk = S)
public void setVersionMediaPerformanceClass() {
ShadowBuild.setVersionMediaPerformanceClass(R);
assertThat(VERSION.MEDIA_PERFORMANCE_CLASS).isEqualTo(R);
}

@Test
@Config(minSdk = M)
public void setVersionSecurityPatch() {
Expand Down
Expand Up @@ -2,11 +2,18 @@

import static android.os.Build.VERSION_CODES.M;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.robolectric.Shadows.shadowOf;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
Expand All @@ -16,16 +23,30 @@
@RunWith(AndroidJUnit4.class)
@Config(minSdk = M)
public class ShadowIconTest {

private static final int MSG_ICON_LOADED = 312;

public static final int TYPE_BITMAP = 1;
public static final int TYPE_RESOURCE = 2;
public static final int TYPE_DATA = 3;
public static final int TYPE_URI = 4;

@Nullable private Drawable loadedDrawable;

private final Context appContext = ApplicationProvider.getApplicationContext();
private final Handler mainHandler =
new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_ICON_LOADED) {
loadedDrawable = (Drawable) msg.obj;
}
}
};

@Test
public void testGetRes() {
Icon icon =
Icon.createWithResource(
ApplicationProvider.getApplicationContext(), android.R.drawable.ic_delete);
Icon icon = Icon.createWithResource(appContext, android.R.drawable.ic_delete);
assertThat(shadowOf(icon).getType()).isEqualTo(TYPE_RESOURCE);
assertThat(shadowOf(icon).getResId()).isEqualTo(android.R.drawable.ic_delete);
}
Expand Down Expand Up @@ -55,4 +76,32 @@ public void testGetUri() {
assertThat(shadowOf(icon).getType()).isEqualTo(TYPE_URI);
assertThat(shadowOf(icon).getUri()).isEqualTo(uri);
}

@Test
public void testLoadDrawableAsyncWithMessage() {
ShadowIcon.overrideExecutor(directExecutor());

Icon icon = Icon.createWithResource(appContext, android.R.drawable.ic_delete);

Message andThen = Message.obtain(mainHandler, MSG_ICON_LOADED);

icon.loadDrawableAsync(appContext, andThen);
ShadowLooper.idleMainLooper();

assertThat(shadowOf(loadedDrawable).getCreatedFromResId())
.isEqualTo(android.R.drawable.ic_delete);
}

@Test
public void testLoadDrawableAsyncWithListener() {
ShadowIcon.overrideExecutor(directExecutor());

Icon icon = Icon.createWithResource(appContext, android.R.drawable.ic_delete);

icon.loadDrawableAsync(appContext, drawable -> this.loadedDrawable = drawable, mainHandler);
ShadowLooper.idleMainLooper();

assertThat(shadowOf(loadedDrawable).getCreatedFromResId())
.isEqualTo(android.R.drawable.ic_delete);
}
}
Expand Up @@ -204,7 +204,7 @@ public void postAndRemoveSyncBarrierToken() {
}

@Test
// TODO(b/74402484): enable once workaround is removed
// TODO(https://github.com/robolectric/robolectric/issues/6852): enable once workaround is removed
@Ignore
public void removeInvalidSyncBarrierToken() {
try {
Expand Down
Expand Up @@ -82,7 +82,7 @@ private static URL[] getClassPathUrls(ClassLoader classloader) {
return parseJavaClassPath();
}

// TODO(b/65488446): Use a public API once one is available.
// TODO(https://github.com/google/guava/issues/2956): Use a public API once one is available.
private static URL[] parseJavaClassPath() {
ImmutableList.Builder<URL> urls = ImmutableList.builder();
for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
Expand Down

0 comments on commit 1be6910

Please sign in to comment.