Bug 1468048 - Introduce GeckoResult r=droeh,jchen
This is a Promise-like system used to return asynchronous results.
MozReview-Commit-ID: 3ZBP4S7d25A
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoResultTest.java
@@ -0,0 +1,226 @@
+package org.mozilla.geckoview.test;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.GeckoResult.OnExceptionListener;
+import org.mozilla.geckoview.GeckoResult.OnValueListener;
+
+import android.os.Looper;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.UiThreadTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class GeckoResultTest {
+ private static final long DEFAULT_TIMEOUT = 5000;
+
+ @Rule
+ public UiThreadTestRule mUiThreadTestRule = new UiThreadTestRule();
+
+ private boolean mDone;
+
+ private void waitUntilDone() {
+ while (!mDone) {
+ UiThreadUtils.loopUntilIdle(DEFAULT_TIMEOUT);
+ }
+ }
+
+ private void done() {
+ UiThreadUtils.HANDLER.post(new Runnable() {
+ @Override
+ public void run() {
+ mDone = true;
+ }
+ });
+ }
+
+ @Before
+ public void setup() {
+ mDone = false;
+ }
+
+ @Test
+ @UiThreadTest
+ public void thenWithResult() {
+ GeckoResult.fromValue(42).then(new OnValueListener<Integer, Void>() {
+ @Override
+ public GeckoResult<Void> onValue(Integer value) {
+ assertThat("Value should match", value, equalTo(42));
+ done();
+ return null;
+ }
+ });
+
+ assertThat("We should not be done", mDone, equalTo(false));
+ waitUntilDone();
+ }
+
+ @Test
+ @UiThreadTest
+ public void thenWithException() {
+ final Throwable boom = new Exception("boom");
+ GeckoResult.fromException(boom).then(null, new OnExceptionListener<Void>() {
+ @Override
+ public GeckoResult<Void> onException(Throwable error) {
+ assertThat("Exception should match", error, equalTo(boom));
+ done();
+ return null;
+ }
+ });
+
+ assertThat("We should not be done", mDone, equalTo(false));
+ waitUntilDone();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ @UiThreadTest
+ public void thenNoListeners() {
+ GeckoResult.fromValue(42).then(null, null);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testEquals() {
+ final GeckoResult<Integer> result = GeckoResult.fromValue(42);
+ final GeckoResult<Integer> result2 = new GeckoResult<>(result);
+
+ assertThat("Results should be equal", result, equalTo(result2));
+ assertThat("Hashcode should be equal", result.hashCode(), equalTo(result2.hashCode()));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ @UiThreadTest
+ public void completeMultiple() {
+ final GeckoResult<Integer> deferred = new GeckoResult<Integer>();
+ deferred.complete(42);
+ deferred.complete(43);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ @UiThreadTest
+ public void completeMultipleExceptions() {
+ final GeckoResult<Integer> deferred = new GeckoResult<Integer>();
+ deferred.completeExceptionally(new Exception("boom"));
+ deferred.completeExceptionally(new Exception("boom again"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ @UiThreadTest
+ public void completeMixed() {
+ final GeckoResult<Integer> deferred = new GeckoResult<Integer>();
+ deferred.complete(42);
+ deferred.completeExceptionally(new Exception("boom again"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ @UiThreadTest
+ public void completeExceptionallyNull() {
+ new GeckoResult<Integer>().completeExceptionally(null);
+ }
+
+ @Test
+ @UiThreadTest
+ public void completeThreaded() {
+ final GeckoResult<Integer> deferred = new GeckoResult<>();
+ final Thread thread = new Thread() {
+ @Override
+ public void run() {
+ deferred.complete(42);
+ }
+ };
+
+ deferred.then(new OnValueListener<Integer, Void>() {
+ @Override
+ public GeckoResult<Void> onValue(Integer value) {
+ assertThat("Value should match", value, equalTo(42));
+ assertThat("Thread should match", Thread.currentThread(),
+ equalTo(Looper.getMainLooper().getThread()));
+ done();
+ return null;
+ }
+ });
+
+ thread.start();
+ waitUntilDone();
+ }
+
+ @Test
+ @UiThreadTest
+ public void completeExceptionallyThreaded() {
+ final GeckoResult<Integer> deferred = new GeckoResult<>();
+ final Throwable boom = new Exception("boom");
+ final Thread thread = new Thread() {
+ @Override
+ public void run() {
+ deferred.completeExceptionally(boom);
+ }
+ };
+
+ deferred.then(new OnExceptionListener<Void>() {
+ @Override
+ public GeckoResult<Void> onException(Throwable error) {
+ assertThat("Exception should match", error, equalTo(boom));
+ assertThat("Thread should match", Thread.currentThread(),
+ equalTo(Looper.getMainLooper().getThread()));
+ done();
+ return null;
+ }
+ });
+
+ thread.start();
+ waitUntilDone();
+ }
+
+ @UiThreadTest
+ @Test
+ public void resultChaining() {
+ assertThat("We're on the UI thread", Thread.currentThread(), equalTo(Looper.getMainLooper().getThread()));
+
+ GeckoResult.fromValue(42).then(new OnValueListener<Integer, String>() {
+ @Override
+ public GeckoResult<String> onValue(Integer value) {
+ assertThat("Value should match", value, equalTo(42));
+ return GeckoResult.fromValue("hello");
+ }
+ }).then(new OnValueListener<String, Float>() {
+ @Override
+ public GeckoResult<Float> onValue(String value) {
+ assertThat("Value should match", value, equalTo("hello"));
+ return GeckoResult.fromValue(42.0f);
+ }
+ }).then(new OnValueListener<Float, Float>() {
+ @Override
+ public GeckoResult<Float> onValue(Float value) {
+ assertThat("Value should match", value, equalTo(42.0f));
+ return GeckoResult.fromException(new Exception("boom"));
+ }
+ }).then(new OnExceptionListener<MockException>() {
+ @Override
+ public GeckoResult<MockException> onException(Throwable error) {
+ assertThat("Error message should match", error.getMessage(), equalTo("boom"));
+ throw new MockException();
+ }
+ }).then(new OnExceptionListener<Void>() {
+ @Override
+ public GeckoResult<Void> onException(Throwable exception) {
+ assertThat("Exception should be MockException", exception instanceof MockException, equalTo(true));
+ done();
+ return null;
+ }
+ });
+
+ waitUntilDone();
+ }
+
+ private static class MockException extends RuntimeException {
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
@@ -0,0 +1,303 @@
+package org.mozilla.geckoview;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+
+/**
+ * GeckoResult is a class that represents an asynchronous result.
+ *
+ * @param <T> The type of the value delivered via the GeckoResult.
+ */
+public class GeckoResult<T> {
+ private static final String LOGTAG = "GeckoResult";
+
+ private static Handler sUIHandler;
+ private static synchronized Handler getUIHandler() {
+ if (sUIHandler == null) {
+ sUIHandler = new Handler(Looper.getMainLooper());
+ }
+
+ return sUIHandler;
+ }
+
+ private boolean mComplete;
+ private T mValue;
+ private Throwable mError;
+
+ private final ArrayList<Runnable> mListeners;
+
+ /**
+ * This constructs an incomplete GeckoResult. Call {@link #complete(Object)} or
+ * {@link #completeExceptionally(Throwable)} in order to fulfill the result.
+ */
+ public GeckoResult() {
+ mListeners = new ArrayList<>();
+ }
+
+ /**
+ * This constructs a result from another result. Listeners are not copied.
+ *
+ * @param from The {@link GeckoResult} to copy.
+ */
+ public GeckoResult(GeckoResult<T> from) {
+ this();
+ mComplete = from.mComplete;
+ mValue = from.mValue;
+ mError = from.mError;
+ }
+
+ /**
+ * This constructs a result that is completed with the specified value.
+ *
+ * @param value The value used to complete the newly created result.
+ * @return The completed {@link GeckoResult}
+ */
+ public static <U> GeckoResult<U> fromValue(final U value) {
+ final GeckoResult<U> result = new GeckoResult<>();
+ result.complete(value);
+ return result;
+ }
+
+ /**
+ * This constructs a result that is completed with the specified {@link Throwable}.
+ * May not be null.
+ *
+ * @param error The exception used to complete the newly created result.
+ * @return The completed {@link GeckoResult}
+ */
+ public static <T> GeckoResult<T> fromException(@NonNull final Throwable error) {
+ final GeckoResult<T> result = new GeckoResult<>();
+ result.completeExceptionally(error);
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (mComplete ? 1 : 0);
+ result = 31 * result + (mValue != null ? mValue.hashCode() : 0);
+ result = 31 * result + (mError != null ? mError.hashCode() : 0);
+ return result;
+ }
+
+ // This can go away once we can rely on java.util.Objects.equals() (API 19)
+ private static boolean objectEquals(final Object a, final Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (other instanceof GeckoResult<?>) {
+ final GeckoResult<?> result = (GeckoResult<?>)other;
+ return result.mComplete == mComplete &&
+ objectEquals(result.mError, mError) &&
+ objectEquals(result.mValue, mValue);
+ }
+
+ return false;
+ }
+
+ /**
+ * Convenience method for {@link #then(OnValueListener, OnExceptionListener)}.
+ *
+ * @param valueListener An instance of {@link OnValueListener}, called when the
+ * {@link GeckoResult} is completed with a value.
+ * @param <U>
+ * @return
+ */
+ public synchronized <U> GeckoResult<U> then(@NonNull final OnValueListener<T, U> valueListener) {
+ return then(valueListener, null);
+ }
+
+ /**
+ * Convenience method for {@link #then(OnValueListener, OnExceptionListener)}.
+ *
+ * @param exceptionListener An instance of {@link OnExceptionListener}, called when the
+ * {@link GeckoResult} is completed with an {@link Exception}.
+ * @param <U> The type contained in the returned {@link GeckoResult}
+ * @return
+ */
+ public synchronized <U> GeckoResult<U> then(@NonNull final OnExceptionListener<U> exceptionListener) {
+ return then(null, exceptionListener);
+ }
+
+ /**
+ * Adds listeners to be called when the {@link GeckoResult} is completed either with
+ * a value or {@link Throwable}. Listeners will be invoked on the main thread. If the
+ * result is already complete when this method is called, listeners will be invoked in
+ * a future {@link Looper} iteration.
+ *
+ * @param valueListener An instance of {@link OnValueListener}, called when the
+ * {@link GeckoResult} is completed with a value.
+ * @param exceptionListener An instance of {@link OnExceptionListener}, called when the
+ * {@link GeckoResult} is completed with an {@link Throwable}.
+ * @param <U> The type contained in the returned {@link GeckoResult}
+ */
+ public synchronized <U> GeckoResult<U> then(@Nullable final OnValueListener<T, U> valueListener,
+ @Nullable final OnExceptionListener<U> exceptionListener) {
+ if (valueListener == null && exceptionListener == null) {
+ throw new IllegalArgumentException("At least one listener should be non-null");
+ }
+
+ final GeckoResult<U> result = new GeckoResult<U>();
+ final Runnable listener = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (valueListener != null && haveValue()) {
+ result.completeFrom(valueListener.onValue(mValue));
+ } else if (exceptionListener != null && haveError()) {
+ result.completeFrom(exceptionListener.onException(mError));
+ }
+ } catch (Exception e) {
+ if (!result.mComplete) {
+ result.completeExceptionally(e);
+ }
+ }
+ }
+ };
+
+ if (mComplete) {
+ dispatch(listener);
+ } else {
+ mListeners.add(listener);
+ }
+
+ return result;
+ }
+
+ private synchronized void dispatch() {
+ if (!mComplete) {
+ throw new IllegalStateException("Cannot dispatch unless result is complete");
+ }
+
+ final ArrayList<Runnable> listeners = new ArrayList<>(mListeners);
+ mListeners.clear();
+
+ getUIHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ for (final Runnable listener : listeners) {
+ listener.run();
+ }
+ }
+ });
+ }
+
+ private synchronized void dispatch(final Runnable runnable) {
+ if (!mComplete) {
+ throw new IllegalStateException("Cannot dispatch unless result is complete");
+ }
+
+ getUIHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+ }
+ });
+ }
+
+ /**
+ * Completes this result based on another result.
+ *
+ * @param other The result that this result should mirror
+ */
+ private void completeFrom(final GeckoResult<T> other) {
+ if (other == null) {
+ complete(null);
+ return;
+ }
+
+ other.then(new OnValueListener<T, Void>() {
+ @Override
+ public GeckoResult<Void> onValue(T value) {
+ complete(value);
+ return null;
+ }
+ }, new OnExceptionListener<Void>() {
+ @Override
+ public GeckoResult<Void> onException(Throwable error) {
+ completeExceptionally(error);
+ return null;
+ }
+ });
+ }
+
+ /**
+ * This completes the result with the specified value. IllegalStateException is thrown
+ * if the result is already complete.
+ *
+ * @param value The value used to complete the result.
+ * @throws IllegalStateException
+ */
+ public synchronized void complete(final T value) {
+ if (mComplete) {
+ throw new IllegalStateException("result is already complete");
+ }
+
+ mValue = value;
+ mComplete = true;
+
+ dispatch();
+ }
+
+ /**
+ * This completes the result with the specified {@link Throwable}. IllegalStateException is thrown
+ * if the result is already complete.
+ *
+ * @param exception The {@link Throwable} used to complete the result.
+ * @throws IllegalStateException
+ */
+ public synchronized void completeExceptionally(@NonNull final Throwable exception) {
+ if (mComplete) {
+ throw new IllegalStateException("result is already complete");
+ }
+
+ if (exception == null) {
+ throw new IllegalArgumentException("Throwable must not be null");
+ }
+
+ mError = exception;
+ mComplete = true;
+
+ dispatch();
+ }
+
+ /**
+ * An interface used to deliver values to listeners of a {@link GeckoResult}
+ * @param <T> This is the type of the value delivered via {@link #onValue(Object)}
+ * @param <U> This is the type of the value for the result returned from {@link #onValue(Object)}
+ */
+ public interface OnValueListener<T, U> {
+ /**
+ * Called when a {@link GeckoResult} is completed with a value. This will be
+ * called on the same thread in which the result was completed.
+ *
+ * @param value The value of the {@link GeckoResult}
+ * @return A new {@link GeckoResult}, used for chaining results together.
+ * May be null.
+ */
+ GeckoResult<U> onValue(T value);
+ }
+
+ /**
+ * An interface used to deliver exceptions to listeners of a {@link GeckoResult}
+ *
+ * @param <V> This is the type of the vale for the result returned from {@link #onException(Throwable)}
+ */
+ public interface OnExceptionListener<V> {
+ GeckoResult<V> onException(Throwable exception);
+ }
+
+ private boolean haveValue() {
+ return mComplete && mError == null;
+ }
+
+ private boolean haveError() {
+ return mComplete && mError != null;
+ }
+}