diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java
new file mode 100644
index 00000000000..36565a24a75
--- /dev/null
+++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java
@@ -0,0 +1,181 @@
+package org.robolectric.integrationtests.axt;
+
+import static androidx.test.espresso.Espresso.onIdle;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test Espresso IdlingResource support. */
+@RunWith(AndroidJUnit4.class)
+public final class EspressoIdlingResourceTest {
+ private final IdlingRegistry idlingRegistry = IdlingRegistry.getInstance();
+
+ private ExecutorService executor;
+
+ @Before
+ public void setup() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @After
+ public void teardown() {
+ for (IdlingResource resource : idlingRegistry.getResources()) {
+ idlingRegistry.unregister(resource);
+ }
+ for (Looper looper : idlingRegistry.getLoopers()) {
+ idlingRegistry.unregisterLooperAsIdlingResource(looper);
+ }
+ executor.shutdown();
+ }
+
+ @Test
+ public void onIdle_idlingResourceIsIdle_doesntBlock() {
+ AtomicBoolean didCheckIdle = new AtomicBoolean();
+ idlingRegistry.register(
+ new NamedIdleResource("Test", /* isIdle= */ true) {
+ @Override
+ public boolean isIdleNow() {
+ didCheckIdle.set(true);
+ return super.isIdleNow();
+ }
+ });
+
+ onIdle();
+
+ assertThat(didCheckIdle.get()).isTrue();
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ @Test
+ public void onIdle_postToMainThread() {
+ idlingRegistry.register(
+ new NamedIdleResource("Test", /* isIdle= */ false) {
+ boolean submitted;
+
+ @Override
+ public boolean isIdleNow() {
+ if (!submitted) {
+ submitted = true;
+ executor.submit(this::postToMainLooper);
+ }
+ return super.isIdleNow();
+ }
+
+ void postToMainLooper() {
+ new Handler(Looper.getMainLooper()).post(() -> setIdle(true));
+ }
+ });
+
+ onIdle();
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ @Test
+ public void onIdle_cooperativeResources() {
+ NamedIdleResource a = new NamedIdleResource("A", /* isIdle= */ true);
+ NamedIdleResource b = new NamedIdleResource("B", /* isIdle= */ false);
+ NamedIdleResource c = new NamedIdleResource("C", /* isIdle= */ false);
+ idlingRegistry.register(a, b);
+ executor.submit(
+ () -> {
+ a.setIdle(false);
+ b.setIdle(true);
+ c.setIdle(false);
+ executor.submit(
+ () -> {
+ a.setIdle(true);
+ b.setIdle(false);
+ c.setIdle(false);
+ executor.submit(
+ () -> {
+ a.setIdle(true);
+ b.setIdle(true);
+ c.setIdle(true);
+ });
+ });
+ });
+
+ onIdle();
+
+ assertThat(a.isIdleNow()).isTrue();
+ assertThat(b.isIdleNow()).isTrue();
+ assertThat(c.isIdleNow()).isTrue();
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ @Test
+ public void onIdle_looperIsIdle() throws Exception {
+ HandlerThread handlerThread = new HandlerThread("Test");
+ try {
+ handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
+ CountDownLatch handlerStarted = new CountDownLatch(1);
+ CountDownLatch releaseHandler = new CountDownLatch(1);
+ handler.post(
+ () -> {
+ handlerStarted.countDown();
+ try {
+ releaseHandler.await();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ });
+ handlerStarted.await();
+ idlingRegistry.registerLooperAsIdlingResource(handlerThread.getLooper());
+
+ executor.submit(releaseHandler::countDown);
+ onIdle();
+
+ // onIdle should have blocked on the looper waiting on the release latch
+ assertThat(releaseHandler.getCount()).isEqualTo(0);
+ } finally {
+ handlerThread.quit();
+ }
+ }
+
+ private static class NamedIdleResource implements IdlingResource {
+ final String name;
+ final AtomicBoolean isIdle;
+ ResourceCallback callback;
+
+ NamedIdleResource(String name, boolean isIdle) {
+ this.name = name;
+ this.isIdle = new AtomicBoolean(isIdle);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean isIdleNow() {
+ return isIdle.get();
+ }
+
+ void setIdle(boolean isIdle) {
+ this.isIdle.set(isIdle);
+ if (isIdle && callback != null) {
+ callback.onTransitionToIdle();
+ }
+ }
+
+ @Override
+ public void registerIdleTransitionCallback(ResourceCallback callback) {
+ this.callback = callback;
+ }
+ }
+}
diff --git a/robolectric/build.gradle b/robolectric/build.gradle
index 13355388b24..96484372cb7 100755
--- a/robolectric/build.gradle
+++ b/robolectric/build.gradle
@@ -44,6 +44,7 @@ dependencies {
compileOnly AndroidSdk.MAX_SDK.coordinates
compileOnly "junit:junit:${junitVersion}"
implementation "androidx.test:monitor:$axtVersion"
+ implementation "androidx.test.espresso:espresso-idling-resource:3.4.0"
testImplementation "junit:junit:${junitVersion}"
testImplementation "com.google.truth:truth:${truthVersion}"
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java
new file mode 100644
index 00000000000..29de83dec2d
--- /dev/null
+++ b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java
@@ -0,0 +1,24 @@
+package org.robolectric.android.internal;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.Beta;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Timeout exception thrown when idling resources are not idle for longer than the configured
+ * timeout.
+ *
+ *
See {@link androidx.test.espresso.IdlingResourceTimeoutException}.
+ *
+ *
Note: This API may be removed in the future in favor of using espresso's exception directly.
+ */
+@Beta
+public final class IdlingResourceTimeoutException extends RuntimeException {
+ public IdlingResourceTimeoutException(List resourceNames) {
+ super(
+ String.format(
+ Locale.ROOT, "Wait for %s to become idle timed out", checkNotNull(resourceNames)));
+ }
+}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
index f73b0057e3d..f20e3e50842 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
@@ -7,12 +7,16 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Comparator.comparingInt;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
+import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Build.VERSION_CODES;
+import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
@@ -23,15 +27,31 @@
import android.view.WindowManager.LayoutParams;
import android.view.WindowManagerGlobal;
import android.view.WindowManagerImpl;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
import androidx.test.platform.ui.InjectEventSecurityException;
import androidx.test.platform.ui.UiController;
+import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowPausedLooper;
import org.robolectric.util.ReflectionHelpers;
/** A {@link UiController} that runs on a local JVM with Robolectric. */
@@ -46,6 +66,22 @@ public class LocalUiController implements UiController {
private static final Predicate WATCH_TOUCH_OUTSIDE =
IS_TOUCH_MODAL.negate().and(hasLayoutFlag(FLAG_WATCH_OUTSIDE_TOUCH));
+ private static long idlingResourceErrorTimeoutMs = SECONDS.toMillis(26);
+ private final HashSet syncedIdlingResources = new HashSet<>();
+ private final ExecutorService looperIdlingExecutor = Executors.newCachedThreadPool();
+
+ /**
+ * Sets the error timeout for idling resources.
+ *
+ * See {@link androidx.test.espresso.IdlingPolicies#setIdlingResourceTimeout(long, TimeUnit)}.
+ *
+ *
Note: This API may be removed in the future in favor of using IdlingPolicies directly.
+ */
+ @Beta
+ public static void setIdlingResourceTimeout(long timeout, TimeUnit unit) {
+ idlingResourceErrorTimeoutMs = unit.toMillis(timeout);
+ }
+
@Override
public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException {
checkNotNull(event);
@@ -174,7 +210,110 @@ static KeyCharacterMap getKeyCharacterMap() {
@Override
public void loopMainThreadUntilIdle() {
- shadowMainLooper().idle();
+ if (!ShadowLooper.looperMode().equals(LooperMode.Mode.PAUSED)) {
+ shadowMainLooper().idle();
+ } else {
+ ImmutableSet idlingResources = syncIdlingResources();
+ if (idlingResources.isEmpty()) {
+ shadowMainLooper().idle();
+ } else {
+ loopMainThreadUntilIdlingResourcesIdle(idlingResources);
+ }
+ }
+ }
+
+ private void loopMainThreadUntilIdlingResourcesIdle(
+ ImmutableSet idlingResources) {
+ Looper mainLooper = Looper.myLooper();
+ ShadowPausedLooper shadowMainLooper = Shadow.extract(mainLooper);
+ Handler handler = new Handler(mainLooper);
+ Set activeResources = new HashSet<>();
+ long startTimeNanos = System.nanoTime();
+
+ shadowMainLooper.idle();
+ while (true) {
+ // Gather the list of resources that are not idling.
+ for (IdlingResourceProxy resource : idlingResources) {
+ // Add the resource as active and check if it's idle, if it is already is will be removed
+ // synchronously. The idle callback is synchronized in the resource which avoids a race
+ // between registering the idle callback and checking the idle state.
+ activeResources.add(resource);
+ resource.notifyOnIdle(
+ () -> {
+ if (Looper.myLooper() == mainLooper) {
+ activeResources.remove(resource);
+ } else {
+ // Post to restart the main thread.
+ handler.post(() -> activeResources.remove(resource));
+ }
+ });
+ }
+ // If all are idle then just return, we're done.
+ if (activeResources.isEmpty()) {
+ break;
+ }
+ // While the resources that weren't idle haven't transitioned to idle continue to loop the
+ // main looper waiting for any new messages. Once all resources have transitioned to idle loop
+ // around again to make sure all resources are idle at the same time.
+ while (!activeResources.isEmpty()) {
+ long elapsedTimeMs = NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos);
+ if (elapsedTimeMs >= idlingResourceErrorTimeoutMs) {
+ throw new IdlingResourceTimeoutException(idlingResourceNames(activeResources));
+ }
+ // Poll the queue and suspend the thread until we get new messages or the idle transition.
+ shadowMainLooper.poll(idlingResourceErrorTimeoutMs - elapsedTimeMs);
+ shadowMainLooper.idle();
+ }
+ }
+ }
+
+ private ImmutableSet syncIdlingResources() {
+ // Collect unique registered idling resources.
+ HashMap registeredResourceByName = new HashMap<>();
+ for (IdlingResource resource : IdlingRegistry.getInstance().getResources()) {
+ String name = resource.getName();
+ if (registeredResourceByName.containsKey(name)) {
+ logDuplicate(name, registeredResourceByName.get(name), resource);
+ } else {
+ registeredResourceByName.put(name, resource);
+ }
+ }
+ Iterator iterator = syncedIdlingResources.iterator();
+ while (iterator.hasNext()) {
+ IdlingResourceProxyImpl proxy = iterator.next();
+ if (registeredResourceByName.get(proxy.name) == proxy.resource) {
+ // Already registered, don't need to add.
+ registeredResourceByName.remove(proxy.name);
+ } else {
+ // Previously registered, but no longer registered, remove.
+ iterator.remove();
+ }
+ }
+ // Add new idling resources that weren't previously registered.
+ for (Map.Entry entry : registeredResourceByName.entrySet()) {
+ syncedIdlingResources.add(new IdlingResourceProxyImpl(entry.getKey(), entry.getValue()));
+ }
+
+ return ImmutableSet.builder()
+ .addAll(syncedIdlingResources)
+ .addAll(
+ IdlingRegistry.getInstance().getLoopers().stream()
+ .map(LooperIdlingResource::new)
+ .iterator())
+ .build();
+ }
+
+ private static void logDuplicate(String name, IdlingResource a, IdlingResource b) {
+ Log.e(
+ TAG,
+ String.format(
+ "Attempted to register resource with same names:"
+ + " %s. R1: %s R2: %s.\nDuplicate resource registration will be ignored.",
+ name, a, b));
+ }
+
+ private static List idlingResourceNames(Set idlingResources) {
+ return idlingResources.stream().map(IdlingResourceProxy::getName).collect(toList());
}
@Override
@@ -275,4 +414,88 @@ boolean watchTouchOutside() {
return WATCH_TOUCH_OUTSIDE.test(this);
}
}
+
+ private interface IdlingResourceProxy {
+ String getName();
+
+ void notifyOnIdle(Runnable idleCallback);
+ }
+
+ private static final class IdlingResourceProxyImpl implements IdlingResourceProxy {
+ private final String name;
+ private final IdlingResource resource;
+
+ private Runnable idleCallback;
+
+ IdlingResourceProxyImpl(String name, IdlingResource resource) {
+ this.name = name;
+ this.resource = resource;
+ resource.registerIdleTransitionCallback(this::onIdle);
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public synchronized void notifyOnIdle(Runnable idleCallback) {
+ if (resource.isIdleNow()) {
+ this.idleCallback = null;
+ idleCallback.run();
+ } else {
+ this.idleCallback = idleCallback;
+ }
+ }
+
+ private synchronized void onIdle() {
+ if (idleCallback != null) {
+ idleCallback.run();
+ idleCallback = null;
+ }
+ }
+ }
+
+ private final class LooperIdlingResource implements IdlingResourceProxy {
+ private final Looper looper;
+ private final ShadowLooper shadowLooper;
+ private Runnable idleCallback;
+
+ LooperIdlingResource(Looper looper) {
+ this.looper = looper;
+ this.shadowLooper = shadowOf(looper);
+ }
+
+ @Override
+ public String getName() {
+ return looper.toString();
+ }
+
+ @Override
+ public synchronized void notifyOnIdle(Runnable idleCallback) {
+ if (shadowLooper.isIdle()) {
+ this.idleCallback = null;
+ idleCallback.run();
+ } else {
+ this.idleCallback = idleCallback;
+ // Note idle() doesn't throw an exception if called from another thread, the looper would
+ // die with an unhandled exception.
+ // TODO(paulsowden): It's not technically necessary to idle the looper from another thread,
+ // it can be idled from its own thread, however we'll need API access to do this and
+ // observe the idle state--the idle() api blocks the calling thread by default. Perhaps a
+ // ListenableFuture idleAsync() variant?
+ looperIdlingExecutor.execute(this::idleLooper);
+ }
+ }
+
+ private void idleLooper() {
+ shadowLooper.idle();
+ synchronized (this) {
+ if (idleCallback != null) {
+ idleCallback.run();
+ idleCallback = null;
+ }
+ }
+ }
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapFactoryTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapFactoryTest.java
index 2d0e5e41e4d..c19b5d286b9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapFactoryTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapFactoryTest.java
@@ -136,7 +136,7 @@ public void decodeBytes_shouldSetDescriptionAndCreatedFrom() throws Exception {
byte[] yummyBites = "Hi!".getBytes("UTF-8");
Bitmap bitmap = BitmapFactory.decodeByteArray(yummyBites, 100, 100);
ShadowBitmap shadowBitmap = shadowOf(bitmap);
- assertEquals("Bitmap for Hi! bytes 100..100", shadowBitmap.getDescription());
+ assertEquals("Bitmap for 3 bytes 100..100", shadowBitmap.getDescription());
assertEquals(yummyBites, shadowBitmap.getCreatedFromBytes());
assertEquals(100, bitmap.getWidth());
assertEquals(100, bitmap.getHeight());
@@ -148,7 +148,7 @@ public void decodeBytes_shouldSetDescriptionAndCreatedFromWithOptions() throws E
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeByteArray(yummyBites, 100, 100, options);
ShadowBitmap shadowBitmap = shadowOf(bitmap);
- assertEquals("Bitmap for Hi! bytes 100..100", shadowBitmap.getDescription());
+ assertEquals("Bitmap for 3 bytes 100..100", shadowBitmap.getDescription());
assertEquals(yummyBites, shadowBitmap.getCreatedFromBytes());
assertEquals(100, bitmap.getWidth());
assertEquals(100, bitmap.getHeight());
@@ -263,7 +263,7 @@ public void decodeByteArray_shouldGetWidthAndHeightFromHints() {
byte[] bytes = data.getBytes(UTF_8);
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
- assertEquals("Bitmap for " + data, shadowOf(bitmap).getDescription());
+ assertEquals("Bitmap for " + bytes.length + " bytes", shadowOf(bitmap).getDescription());
assertEquals(123, bitmap.getWidth());
assertEquals(456, bitmap.getHeight());
}
@@ -275,7 +275,7 @@ public void decodeByteArray_shouldIncludeOffsets() {
byte[] bytes = data.getBytes(UTF_8);
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 1, bytes.length - 2);
- assertEquals("Bitmap for " + data + " bytes 1..13", shadowOf(bitmap).getDescription());
+ assertEquals("Bitmap for " + bytes.length + " bytes 1..13", shadowOf(bitmap).getDescription());
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
index 1e23ef7aa23..cadc3e96952 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
@@ -25,6 +25,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -544,6 +545,26 @@ public void testIdleNotStuck_whenThreadCrashes() throws Exception {
assertThat(thread.getState()).isEqualTo(Thread.State.TERMINATED);
}
+ @Test
+ public void poll() {
+ ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper());
+ AtomicBoolean backgroundThreadPosted = new AtomicBoolean();
+ AtomicBoolean foregroundThreadReceived = new AtomicBoolean();
+ shadowPausedLooper.idle();
+
+ new Handler(handlerThread.getLooper())
+ .post(
+ () -> {
+ backgroundThreadPosted.set(true);
+ new Handler(Looper.getMainLooper()).post(() -> foregroundThreadReceived.set(true));
+ });
+ shadowPausedLooper.poll(0);
+ shadowPausedLooper.idle();
+
+ assertThat(backgroundThreadPosted.get()).isTrue();
+ assertThat(foregroundThreadReceived.get()).isTrue();
+ }
+
private static class BlockingRunnable implements Runnable {
CountDownLatch latch = new CountDownLatch(1);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
index b33e57ee300..a966ff5a651 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
@@ -142,7 +142,7 @@ protected static Bitmap decodeFileDescriptor(
}
return null;
}
- Bitmap bitmap = create("fd:" + fd, outPadding, opts, null, image);
+ Bitmap bitmap = create("fd:" + fd, null, outPadding, opts, null, image);
ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromFileDescriptor = fd;
return bitmap;
@@ -186,7 +186,7 @@ protected static Bitmap decodeStream(
}
return null;
}
- Bitmap bitmap = create(name, outPadding, opts, null, image);
+ Bitmap bitmap = create(name, null, outPadding, opts, null, image);
ReflectionHelpers.callInstanceMethod(
bitmap, "setNinePatchChunk", ClassParameter.from(byte[].class, ninePatchChunk));
ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
@@ -207,10 +207,10 @@ protected static Bitmap decodeByteArray(byte[] data, int offset, int length) {
@Implementation
protected static Bitmap decodeByteArray(
byte[] data, int offset, int length, BitmapFactory.Options opts) {
- String desc = new String(data, UTF_8);
+ String desc = data.length + " bytes";
if (offset != 0 || length != data.length) {
- desc += " bytes " + offset + ".." + length;
+ desc += " " + offset + ".." + length;
}
ByteArrayInputStream is = new ByteArrayInputStream(data, offset, length);
@@ -222,7 +222,7 @@ protected static Bitmap decodeByteArray(
}
return null;
}
- Bitmap bitmap = create(desc, opts, image);
+ Bitmap bitmap = create(desc, data, null, opts, null, image);
ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromBytes = data;
return bitmap;
@@ -248,18 +248,19 @@ public static Bitmap create(String name, BitmapFactory.Options options) {
@Deprecated
public static Bitmap create(
final String name, final BitmapFactory.Options options, final Point widthAndHeight) {
- return create(name, null, options, widthAndHeight, null);
+ return create(name, null, null, options, widthAndHeight, null);
}
private static Bitmap create(
final String name,
final BitmapFactory.Options options,
final RobolectricBufferedImage image) {
- return create(name, null, options, null, image);
+ return create(name, null, null, options, null, image);
}
private static Bitmap create(
final String name,
+ byte[] bytes,
final Rect outPadding,
final BitmapFactory.Options options,
final Point widthAndHeightOverride,
@@ -282,7 +283,7 @@ private static Bitmap create(
shadowBitmap.appendDescription(optionsString);
}
- Point p = new Point(selectWidthAndHeight(name, widthAndHeightOverride, image));
+ Point p = new Point(selectWidthAndHeight(name, bytes, widthAndHeightOverride, image));
if (options != null && options.inSampleSize > 1) {
p.x = p.x / options.inSampleSize;
p.y = p.y / options.inSampleSize;
@@ -373,11 +374,15 @@ public static void reset() {
private static Point selectWidthAndHeight(
final String name,
+ byte[] bytes,
final Point widthAndHeightOverride,
final RobolectricBufferedImage robolectricBufferedImage) {
- final Point widthAndHeightFromMap = widthAndHeightMap.get(name);
- if (widthAndHeightFromMap != null) {
- return widthAndHeightFromMap;
+ if (!widthAndHeightMap.isEmpty()) {
+ String sizeKey = bytes == null ? name : new String(bytes, UTF_8);
+ final Point widthAndHeightFromMap = widthAndHeightMap.get(sizeKey);
+ if (widthAndHeightFromMap != null) {
+ return widthAndHeightFromMap;
+ }
}
if (robolectricBufferedImage != null) {
return robolectricBufferedImage.getWidthAndHeight();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
index b2bf2322f02..df4e15b583f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static com.google.common.base.Preconditions.checkState;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;
@@ -206,6 +207,41 @@ public void runPaused(Runnable runnable) {
}
}
+ /**
+ * Polls the message queue waiting until a message is posted to the head of the queue. This will
+ * suspend the thread until a new message becomes available. Returns immediately if the queue is
+ * not idle. There's no guarantee that the message queue will not still be idle when returning,
+ * but if the message queue becomes not idle it will return immediately.
+ *
+ * This method is only applicable for the main looper's queue when called on the main thread,
+ * as the main looper in Robolectric is processed manually (it doesn't loop)--looper threads are
+ * using the native polling of their loopers. Throws an exception if called for another looper's
+ * queue. Non-main thread loopers should use {@link #unPause()}.
+ *
+ *
This should be used with care, it can be used to suspend the main (i.e. test) thread while
+ * worker threads perform some work, and then resumed by posting to the main looper. Used in a
+ * loop to wait on some condition it can process messages on the main looper, simulating the
+ * behavior of the real looper, for example:
+ *
+ *
{@code
+ * while (!condition) {
+ * shadowMainLooper.poll(timeout);
+ * shadowMainLooper.idle();
+ * }
+ * }
+ *
+ * Beware though that a message must be posted to the main thread after the condition is
+ * satisfied, or the condition satisfied while idling the main thread, otherwise the main thread
+ * will continue to be suspended until the timeout.
+ *
+ * @param timeout Timeout in milliseconds, the maximum time to wait before returning, or 0 to wait
+ * indefinitely,
+ */
+ public void poll(long timeout) {
+ checkState(Looper.myLooper() == Looper.getMainLooper() && Looper.myLooper() == realLooper);
+ shadowQueue().poll(timeout);
+ }
+
@Override
public Duration getNextScheduledTaskTime() {
return shadowQueue().getNextScheduledTaskTime();
@@ -344,7 +380,7 @@ private class RunOneRunnable extends ControlRunnable {
@Override
public void run() {
try {
- Message msg = shadowQueue().poll();
+ Message msg = shadowQueue().getNextIgnoringWhen();
if (msg != null) {
SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
msg.getTarget().dispatchMessage(msg);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
index 2c378663f70..8ddcc7bc229 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -6,10 +6,12 @@
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
+import static com.google.common.base.Preconditions.checkState;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;
+import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.MessageQueue.IdleHandler;
@@ -23,6 +25,7 @@
import org.robolectric.annotation.RealObject;
import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Scheduler;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
@@ -33,6 +36,7 @@
*
*
This class should not be referenced directly. Use {@link ShadowMessageQueue} instead.
*/
+@SuppressWarnings("SynchronizeOnNonFinalField")
@Implements(value = MessageQueue.class, isInAndroidSdk = false, looseSignatures = true)
public class ShadowPausedMessageQueue extends ShadowMessageQueue {
@@ -104,6 +108,40 @@ protected void nativePollOnce(long ptr, int timeoutMillis) {
}
}
+ /**
+ * Polls the message queue waiting until a message is posted to the head of the queue. This will
+ * suspend the thread until a new message becomes available. Returns immediately if the queue is
+ * not idle. There's no guarantee that the message queue will not still be idle when returning,
+ * but if the message queue becomes not idle it will return immediately.
+ *
+ *
See {@link ShadowPausedLooper#poll(long)} for more information.
+ *
+ * @param timeout Timeout in milliseconds, the maximum time to wait before returning, or 0 to wait
+ * indefinitely,
+ */
+ void poll(long timeout) {
+ checkState(Looper.myLooper() == Looper.getMainLooper() && Looper.myQueue() == realQueue);
+ // Message queue typically expects the looper to loop calling next() which returns current
+ // messages from the head of the queue. If no messages are current it will mark itself blocked
+ // and call nativePollOnce (see above) which suspends the thread until the next message's time.
+ // When messages are posted to the queue, if a new message is posted to the head and the queue
+ // is marked as blocked, then the enqueue function will notify and resume next(), allowing it
+ // return the next message. To simulate this behavior check if the queue is idle and if it is
+ // mark the queue as blocked and wait on a new message.
+ synchronized (realQueue) {
+ if (isIdle()) {
+ ReflectionHelpers.setField(realQueue, "mBlocked", true);
+ try {
+ realQueue.wait(timeout);
+ } catch (InterruptedException ignored) {
+ // Fall through and unblock with no messages.
+ } finally {
+ ReflectionHelpers.setField(realQueue, "mBlocked", false);
+ }
+ }
+ }
+ }
+
@Implementation(maxSdk = JELLY_BEAN_MR1)
protected void nativeWake(int ptr) {
synchronized (realQueue) {
@@ -272,7 +310,12 @@ public int internalGetSize() {
return count;
}
- Message poll() {
+ /**
+ * Returns the message at the head of the queue immediately, regardless of its scheduled time.
+ * Compare to {@link #getNext()} which will only return the next message if the system clock is
+ * advanced to its scheduled time.
+ */
+ Message getNextIgnoringWhen() {
synchronized (realQueue) {
Message head = getMessages();
if (head != null) {