Bug 1468048 - Introduce GeckoResult r=droeh,jchen draft
authorJames Willcox <snorp@snorp.net>
Sat, 09 Jun 2018 18:21:31 -0500
changeset 814637 ffefc8dbf983261247c9fbe923c7376c4c50b177
parent 814636 e6ca5e30ed54c2a8c61c115fbe79220b69be223f
child 814638 0b17d1e111679869d1cbc2eac3e59d70d63e7c42
push id115291
push userbmo:snorp@snorp.net
push dateThu, 05 Jul 2018 20:44:33 +0000
reviewersdroeh, jchen
bugs1468048
milestone63.0a1
Bug 1468048 - Introduce GeckoResult r=droeh,jchen This is a Promise-like system used to return asynchronous results. MozReview-Commit-ID: 3ZBP4S7d25A
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoResultTest.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
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;
+    }
+}