Skip to content

Commit

Permalink
Use Activity's member Instrumentation in ActivityController
Browse files Browse the repository at this point in the history
Previously, ActivityController was using the global Instrumentation object,
which is replaced after each test. This contributed to an issue where an
Activity that leaked across tests caused illegal states to exist in
ActivityLifecycleMonitor. Instead, use the Activity's own Instrumentation
object, which will have a consistent view of the Activity's lifecycle.

PiperOrigin-RevId: 410271639
  • Loading branch information
hoisie authored and copybara-robolectric committed Nov 16, 2021
1 parent 326991f commit ee348eb
Show file tree
Hide file tree
Showing 19 changed files with 155 additions and 58 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
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
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 Down Expand Up @@ -502,8 +501,9 @@ 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();
}

/** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */
Expand Down
Expand Up @@ -3,6 +3,7 @@
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.S;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.TargetApi;
Expand Down Expand Up @@ -110,6 +111,18 @@ public static void setVersionIncremental(String versionIncremental) {
ReflectionHelpers.setStaticField(Build.VERSION.class, "INCREMENTAL", versionIncremental);
}

/**
* Sets the value of the {@link Build.VERSION#MEDIA_PERFORMANCE_CLASS} field. Available in Android
* S+.
*
* <p>It will be reset for the next test.
*/
@TargetApi(S)
public static void setVersionMediaPerformanceClass(int performanceClass) {
ReflectionHelpers.setStaticField(
Build.VERSION.class, "MEDIA_PERFORMANCE_CLASS", performanceClass);
}

/**
* Sets the value of the {@link Build.VERSION#RELEASE} field.
*
Expand Down
Expand Up @@ -3,9 +3,16 @@
import static android.os.Build.VERSION_CODES.M;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.Icon.OnDrawableLoadedListener;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.Nullable;
import java.util.concurrent.Executor;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
Expand All @@ -17,6 +24,13 @@
@Implements(value = Icon.class, minSdk = M)
public class ShadowIcon {

@Nullable private static Executor executorOverride;

/** Set the executor where async drawable loading will run. */
public static void overrideExecutor(Executor executor) {
executorOverride = executor;
}

@RealObject private Icon realIcon;

@HiddenApi
Expand Down Expand Up @@ -61,6 +75,33 @@ public byte[] getDataBytes() {
return reflector(IconReflector.class, realIcon).getDataBytes();
}

@Implementation
protected void loadDrawableAsync(Context context, Message andThen) {
if (executorOverride != null) {
executorOverride.execute(
() -> {
andThen.obj = realIcon.loadDrawable(context);
andThen.sendToTarget();
});
} else {
reflector(IconReflector.class, realIcon).loadDrawableAsync(context, andThen);
}
}

@Implementation
protected void loadDrawableAsync(
Context context, final OnDrawableLoadedListener listener, Handler handler) {
if (executorOverride != null) {
executorOverride.execute(
() -> {
Drawable result = realIcon.loadDrawable(context);
handler.post(() -> listener.onDrawableLoaded(result));
});
} else {
reflector(IconReflector.class, realIcon).loadDrawableAsync(context, listener, handler);
}
}

@ForType(Icon.class)
interface IconReflector {

Expand All @@ -84,5 +125,12 @@ interface IconReflector {

@Direct
byte[] getDataBytes();

@Direct
void loadDrawableAsync(Context context, Message andThen);

@Direct
void loadDrawableAsync(
Context context, final OnDrawableLoadedListener listener, Handler handler);
}
}
Expand Up @@ -166,7 +166,8 @@ private static void dispatchMessage(Message msg) {
@Implementation
@HiddenApi
protected void removeSyncBarrier(int token) {
// TODO(b/74402484): workaround scheduler corruption of message queue
// TODO(https://github.com/robolectric/robolectric/issues/6852): workaround scheduler corruption
// of message queue
try {
reflector(MessageQueueReflector.class, realQueue).removeSyncBarrier(token);
} catch (IllegalStateException e) {
Expand Down
Expand Up @@ -89,7 +89,7 @@ public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) {
// classloader" behavior.
ClassLoader parcelableClassLoader = (loader == null ? getClass().getClassLoader() : loader);
// Avoid initializing the Parcelable class until we know it implements
// Parcelable and has the necessary CREATOR field. http://b/1171613.
// Parcelable and has the necessary CREATOR field.
Class<?> parcelableClass = Class.forName(name, false /* initialize */, parcelableClassLoader);
if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
throw new BadParcelableException(
Expand Down
Expand Up @@ -15,6 +15,6 @@ public class ShadowSQLiteOpenHelper {
@Implementation(minSdk = O_MR1)
protected void setIdleConnectionTimeout(long idleConnectionTimeoutMs) {
// Calling the real one currently results in a Robolectric deadlock. Just ignore it.
// See b/78464547 .
// See https://github.com/robolectric/robolectric/issues/6853.
}
}

0 comments on commit ee348eb

Please sign in to comment.