Bug 1553515 - Add GeckoResult.accept. r=snorp
☠☠ backed out by 1c2e073c6478 ☠ ☠
authorAgi Sferro <agi@sferro.dev>
Thu, 27 Jun 2019 15:58:16 +0000
changeset 543224 9a058acc45e858bd3a41c691d41e7abe338fe6ca
parent 543223 5a9d7367edb7d4841f655c8acaa51945a3e1d806
child 543225 7a1fb1222e52b1ab29cc0675fd3b5da803ebf70d
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1553515
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1553515 - Add GeckoResult.accept. r=snorp Differential Revision: https://phabricator.services.mozilla.com/D32582
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoResultTest.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionEventDispatcher.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoResultTest.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoResultTest.java
@@ -52,170 +52,132 @@ public class GeckoResultTest {
     @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;
-            }
+        GeckoResult.fromValue(42).accept(value -> {
+            assertThat("Value should match", value, equalTo(42));
+            done();
         });
 
         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;
-            }
+        GeckoResult.fromException(boom).accept(null, error -> {
+            assertThat("Exception should match", error, equalTo(boom));
+            done();
         });
 
         waitUntilDone();
     }
 
     @Test(expected = IllegalArgumentException.class)
     @UiThreadTest
     public void thenNoListeners() {
         GeckoResult.fromValue(42).then(null, null);
     }
 
     @Test
     @UiThreadTest
     public void testCopy() {
         final GeckoResult<Integer> result = new GeckoResult<>(GeckoResult.fromValue(42));
-        result.then(new OnValueListener<Integer, Void>() {
-            @Override
-            public GeckoResult<Void> onValue(Integer value) throws Throwable {
-                assertThat("Value should match", value, equalTo(42));
-                done();
-                return null;
-            }
+        result.accept(value -> {
+            assertThat("Value should match", value, equalTo(42));
+            done();
         });
 
         waitUntilDone();
     }
 
     @Test(expected = IllegalStateException.class)
     @UiThreadTest
     public void completeMultiple() {
-        final GeckoResult<Integer> deferred = new GeckoResult<Integer>();
+        final GeckoResult<Integer> deferred = new GeckoResult<>();
         deferred.complete(42);
         deferred.complete(43);
     }
 
     @Test(expected = IllegalStateException.class)
     @UiThreadTest
     public void completeMultipleExceptions() {
-        final GeckoResult<Integer> deferred = new GeckoResult<Integer>();
+        final GeckoResult<Integer> deferred = new GeckoResult<>();
         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>();
+        final GeckoResult<Integer> deferred = new GeckoResult<>();
         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);
-            }
-        };
+        final Thread thread = new Thread(() -> 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;
-            }
+        deferred.accept(value -> {
+            assertThat("Value should match", value, equalTo(42));
+            ThreadUtils.assertOnUiThread();
+            done();
         });
 
         thread.start();
         waitUntilDone();
     }
 
     @Test
     @UiThreadTest
     public void dispatchOnInitialThread() throws InterruptedException {
-        final Thread thread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                Looper.prepare();
-                final Thread dispatchThread = Thread.currentThread();
+        final Thread thread = new Thread(() -> {
+            Looper.prepare();
+            final Thread dispatchThread = Thread.currentThread();
 
-                GeckoResult.fromValue(42).then(new OnValueListener<Integer, Void>() {
-                    @Override
-                    public GeckoResult<Void> onValue(Integer value) throws Throwable {
-                        assertThat("Thread should match", Thread.currentThread(),
-                                equalTo(dispatchThread));
-                        Looper.myLooper().quit();
-                        return null;
-                    }
-                });
+            GeckoResult.fromValue(42).accept(value -> {
+                assertThat("Thread should match", Thread.currentThread(),
+                        equalTo(dispatchThread));
+                Looper.myLooper().quit();
+            });
 
-                Looper.loop();
-            }
+            Looper.loop();
         });
 
         thread.start();
         thread.join();
     }
 
     @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);
-            }
-        };
+        final Thread thread = new Thread(() -> deferred.completeExceptionally(boom));
 
-        deferred.exceptionally(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;
-            }
+        deferred.exceptionally(error -> {
+            assertThat("Exception should match", error, equalTo(boom));
+            ThreadUtils.assertOnUiThread();
+            done();
+            return null;
         });
 
         thread.start();
         waitUntilDone();
     }
 
     @UiThreadTest
     @Test
@@ -229,100 +191,69 @@ public class GeckoResultTest {
             assertThat("Value should match", value, equalTo("hello"));
             return GeckoResult.fromValue(42.0f);
         }).then(value -> {
             assertThat("Value should match", value, equalTo(42.0f));
             return GeckoResult.fromException(new Exception("boom"));
         }).exceptionally(error -> {
             assertThat("Error message should match", error.getMessage(), equalTo("boom"));
             throw new MockException();
-        }).exceptionally(exception -> {
+        }).accept(null, exception -> {
             assertThat("Exception should be MockException", exception, instanceOf(MockException.class));
             done();
-            return null;
         });
 
         waitUntilDone();
     }
 
     @UiThreadTest
     @Test
     public void then_propagatedValue() {
         // The first GeckoResult only has an exception listener, so when the value 42 is
         // propagated to subsequent GeckoResult instances, the propagated value is coerced to null.
-        GeckoResult.fromValue(42).exceptionally(new OnExceptionListener<String>() {
-            @Override
-            public GeckoResult<String> onException(Throwable exception) throws Throwable {
-                return null;
-            }
-        }).then(new OnValueListener<String, Void>() {
-            @Override
-            public GeckoResult<Void> onValue(String value) throws Throwable {
-                assertThat("Propagated value is null", value, nullValue());
-                done();
-                return null;
-            }
+        GeckoResult.fromValue(42).exceptionally(error -> null)
+        .accept(value -> {
+            assertThat("Propagated value is null", value, nullValue());
+            done();
         });
 
         waitUntilDone();
     }
 
     @UiThreadTest
     @Test(expected = GeckoResult.UncaughtException.class)
     public void then_uncaughtException() {
-        GeckoResult.fromValue(42).then(new OnValueListener<Integer, String>() {
-            @Override
-            public GeckoResult<String> onValue(Integer value) {
-                throw new MockException();
-            }
+        GeckoResult.fromValue(42).then(value -> {
+            throw new MockException();
         });
 
         waitUntilDone();
     }
 
     @UiThreadTest
     @Test(expected = GeckoResult.UncaughtException.class)
     public void then_propagatedUncaughtException() {
-        GeckoResult.fromValue(42).then(new OnValueListener<Integer, String>() {
-            @Override
-            public GeckoResult<String> onValue(Integer value) {
-                throw new MockException();
-            }
-        }).then(new OnValueListener<String, Void>() {
-            @Override
-            public GeckoResult<Void> onValue(String value) throws Throwable {
-                return null;
-            }
-        });
+        GeckoResult.fromValue(42).then(value -> {
+            throw new MockException();
+        }).accept(value -> {});
 
         waitUntilDone();
     }
 
     @UiThreadTest
     @Test
     public void then_caughtException() {
-        GeckoResult.fromValue(42).then(new OnValueListener<Integer, String>() {
-            @Override
-            public GeckoResult<String> onValue(Integer value) throws Exception {
-                throw new MockException();
-            }
-        }).then(new OnValueListener<String, Void>() {
-            @Override
-            public GeckoResult<Void> onValue(String value) throws Throwable {
-                return null;
-            }
-        }).exceptionally(new OnExceptionListener<Void>() {
-            @Override
-            public GeckoResult<Void> onException(Throwable exception) throws Throwable {
+        GeckoResult.fromValue(42).then(value -> { throw new MockException(); })
+            .accept(value -> {})
+            .exceptionally(exception -> {
                 assertThat("Exception should be expected",
                            exception, instanceOf(MockException.class));
                 done();
                 return null;
-            }
-        });
+            });
 
         waitUntilDone();
     }
 
     @Test(expected = IllegalThreadStateException.class)
     public void noLooperThenThrows() {
         assertThat("We shouldn't have a Looper", Looper.myLooper(), nullValue());
         GeckoResult.fromValue(42).then(value -> null);
@@ -352,21 +283,20 @@ public class GeckoResultTest {
         });
 
         thread.start();
 
         final GeckoResult<Integer> result = GeckoResult.fromValue(42);
         assertThat("We shouldn't have a Looper", result.getLooper(), nullValue());
 
         try {
-            result.withHandler(queue.take()).then(value -> {
+            result.withHandler(queue.take()).accept(value -> {
                 assertThat("Thread should match", Thread.currentThread(), equalTo(thread));
                 assertThat("Value should match", value, equalTo(42));
                 Looper.myLooper().quit();
-                return null;
             });
 
             thread.join();
         } catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
     }
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -1151,35 +1151,27 @@ public class GeckoSessionTestRule implem
 
                 // We're delegating an onNewSession call.
                 // Make sure we wait on the newly opened session, if any.
                 final GeckoSession oldSession = (GeckoSession) args[0];
 
                 @SuppressWarnings("unchecked")
                 final GeckoResult<GeckoSession> result = (GeckoResult<GeckoSession>)returnValue;
                 final GeckoResult<GeckoSession> tmpResult = new GeckoResult<>();
-                result.then(new OnValueListener<GeckoSession, Void>() {
-                    @Override
-                    public GeckoResult<Void> onValue(final GeckoSession newSession) throws Throwable {
-                        tmpResult.complete(newSession);
+                result.accept(session -> {
+                    tmpResult.complete(session);
 
-                        // GeckoSession has already hooked up its then() listener earlier,
-                        // so ours will run after. We can wait for the session to
-                        // open here.
-                        tmpResult.then(new OnValueListener<GeckoSession, Void>() {
-                            @Override
-                            public GeckoResult<Void> onValue(GeckoSession newSession) throws Throwable {
-                                if (oldSession.isOpen() && newSession != null) {
-                                    GeckoSessionTestRule.this.waitForOpenSession(newSession);
-                                }
-                                return null;
-                            }
-                        });
-                        return null;
-                    }
+                    // GeckoSession has already hooked up its then() listener earlier,
+                    // so ours will run after. We can wait for the session to
+                    // open here.
+                    tmpResult.accept(newSession -> {
+                        if (oldSession.isOpen() && newSession != null) {
+                            GeckoSessionTestRule.this.waitForOpenSession(newSession);
+                        }
+                    });
                 });
 
                 return tmpResult;
             }
         };
 
         final Class<?>[] classes = DEFAULT_DELEGATES.toArray(
                 new Class<?>[DEFAULT_DELEGATES.size()]);
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java
@@ -103,30 +103,22 @@ public class UiThreadUtils {
     }
 
     private static class ResultHolder<T> {
         public T value;
         public Throwable error;
         public boolean isComplete;
 
         public ResultHolder(GeckoResult<T> result) {
-            result.then(new GeckoResult.OnValueListener<T, Void>() {
-                @Override
-                public GeckoResult<Void> onValue(T value) {
-                    ResultHolder.this.value = value;
-                    isComplete = true;
-                    return null;
-                }
-            }, new GeckoResult.OnExceptionListener<Void>() {
-                @Override
-                public GeckoResult<Void> onException(Throwable error) {
-                    ResultHolder.this.error = error;
-                    isComplete = true;
-                    return null;
-                }
+            result.accept(value -> {
+                ResultHolder.this.value = value;
+                isComplete = true;
+            }, error -> {
+                ResultHolder.this.error = error;
+                isComplete = true;
             });
         }
     }
 
     public interface Condition {
         boolean test();
     }
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
@@ -7,16 +7,17 @@ import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
 import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
 import java.util.ArrayList;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 
 /**
  * GeckoResult is a class that represents an asynchronous result. The result is initially pending,
  * and at a later time, the result may be completed with {@link #complete a value} or {@link
  * #completeExceptionally an exception} depending on the outcome of the asynchronous operation. For
  * example,<pre>
  * public GeckoResult&lt;Integer&gt; divide(final int dividend, final int divisor) {
  *     final GeckoResult&lt;Integer&gt; result = new GeckoResult&lt;&gt;();
@@ -290,16 +291,73 @@ public class GeckoResult<T> {
      * @param <U> Type of the new result that is returned by the listener.
      * @return A new {@link GeckoResult} that the listener will complete.
      */
     public @NonNull <U> GeckoResult<U> exceptionally(@NonNull final OnExceptionListener<U> exceptionListener) {
         return then(null, exceptionListener);
     }
 
     /**
+     * Replacement for {@link java.util.function.Consumer} for devices with minApi &lt; 24.
+     *
+     * @param <T> the type of the input for this consumer.
+     */
+    // TODO: Remove this when we move to min API 24
+    public interface Consumer<T> {
+        /**
+         * Run this consumer for the given input.
+         *
+         * @param t the input value.
+         */
+        void accept(T t);
+    }
+
+    /**
+     * Convenience method for {@link #accept(Consumer, Consumer)}.
+     *
+     * @param valueListener An instance of {@link Consumer}, called when the
+     *                      {@link GeckoResult} is completed with a value.
+     * @return A new {@link GeckoResult} that the listeners will complete.
+     */
+    public @NonNull GeckoResult<Void> accept(@Nullable final Consumer<T> valueListener) {
+        return accept(valueListener, null);
+    }
+
+    /**
+     * Adds listeners to be called when the {@link GeckoResult} is completed either with
+     * a value or {@link Throwable}. Listeners will be invoked on the {@link Looper} returned from
+     * {@link #getLooper()}. If null, this method will throw {@link IllegalThreadStateException}.
+     *
+     * If the result is already complete when this method is called, listeners will be invoked in
+     * a future {@link Looper} iteration.
+     *
+     * @param valueConsumer An instance of {@link Consumer}, called when the
+     *                      {@link GeckoResult} is completed with a value.
+     * @param exceptionConsumer An instance of {@link Consumer}, called when the
+     *                          {@link GeckoResult} is completed with an {@link Throwable}.
+     * @return A new {@link GeckoResult} that the listeners will complete.
+     */
+    public @NonNull GeckoResult<Void> accept(@Nullable final Consumer<T> valueConsumer,
+                                             @Nullable final Consumer<Throwable> exceptionConsumer) {
+        final OnValueListener<T, Void> valueListener = valueConsumer == null ? null :
+                value -> {
+                    valueConsumer.accept(value);
+                    return null;
+                };
+
+        final OnExceptionListener<Void> exceptionListener = exceptionConsumer == null ? null :
+                value -> {
+                    exceptionConsumer.accept(value);
+                    return null;
+                };
+
+        return then(valueListener, 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 {@link Looper} returned from
      * {@link #getLooper()}. If null, this method will throw {@link IllegalThreadStateException}.
      *
      * 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
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -291,55 +291,33 @@ public class GeckoSession implements Par
                                            message.getString("lastVisitedURL"),
                                            message.getInt("flags"));
 
                     if (result == null) {
                         callback.sendSuccess(false);
                         return;
                     }
 
-                    result.then(new GeckoResult.OnValueListener<Boolean, Void>() {
-                        @Override
-                        public GeckoResult<Void> onValue(final Boolean visited) throws Throwable {
-                            callback.sendSuccess(visited.booleanValue());
-                            return null;
-                        }
-                    }, new GeckoResult.OnExceptionListener<Void>() {
-                        @Override
-                        public GeckoResult<Void> onException(final Throwable exception)
-                                 throws Throwable {
-                            callback.sendSuccess(false);
-                            return null;
-                        }
-                    });
+                    result.accept(
+                            visited -> callback.sendSuccess(visited.booleanValue()),
+                            exception -> callback.sendSuccess(false));
                 } else if ("GeckoView:GetVisited".equals(event)) {
                     final String[] urls = message.getStringArray("urls");
 
                     final GeckoResult<boolean[]> result =
                         delegate.getVisited(GeckoSession.this, urls);
 
                     if (result == null) {
                         callback.sendSuccess(null);
                         return;
                     }
 
-                    result.then(new GeckoResult.OnValueListener<boolean[], Void>() {
-                        @Override
-                        public GeckoResult<Void> onValue(final boolean[] visited) throws Throwable {
-                            callback.sendSuccess(visited);
-                            return null;
-                        }
-                    }, new GeckoResult.OnExceptionListener<Void>() {
-                        @Override
-                        public GeckoResult<Void> onException(final Throwable exception)
-                                throws Throwable {
-                            callback.sendError("Failed to fetch visited statuses for URIs");
-                            return null;
-                        }
-                    });
+                    result.accept(
+                            visited -> callback.sendSuccess(visited),
+                            exception -> callback.sendError("Failed to fetch visited statuses for URIs"));
                 }
             }
         };
 
     private final class WebExtensionListener implements BundleEventListener {
         final private HashMap<String, WebExtension.MessageDelegate> mMessageDelegates;
 
         public WebExtensionListener() {
@@ -543,37 +521,26 @@ public class GeckoSession implements Par
                     final GeckoResult<AllowOrDeny> result =
                         delegate.onLoadRequest(GeckoSession.this, request);
 
                     if (result == null) {
                         callback.sendSuccess(null);
                         return;
                     }
 
-                    result.then(new GeckoResult.OnValueListener<AllowOrDeny, Void>() {
-                        @Override
-                        public GeckoResult<Void> onValue(final AllowOrDeny value) throws Throwable {
-                            ThreadUtils.assertOnUiThread();
-                            if (value == AllowOrDeny.ALLOW) {
-                                callback.sendSuccess(false);
-                            } else  if (value == AllowOrDeny.DENY) {
-                                callback.sendSuccess(true);
-                            } else {
-                                callback.sendError("Invalid response");
-                            }
-                            return null;
+                    result.accept(value -> {
+                        ThreadUtils.assertOnUiThread();
+                        if (value == AllowOrDeny.ALLOW) {
+                            callback.sendSuccess(false);
+                        } else  if (value == AllowOrDeny.DENY) {
+                            callback.sendSuccess(true);
+                        } else {
+                            callback.sendError("Invalid response");
                         }
-                    }, new GeckoResult.OnExceptionListener<Void>() {
-                        @Override
-                        public GeckoResult<Void> onException(final Throwable exception)
-                                throws Throwable {
-                            callback.sendError(exception.getMessage());
-                            return null;
-                        }
-                    });
+                    }, exception -> callback.sendError(exception.getMessage()));
                 } else if ("GeckoView:OnLoadError".equals(event)) {
                     final String uri = message.getString("uri");
                     final long errorCode = message.getLong("error");
                     final int errorModule = message.getInt("errorModule");
                     final int errorClass = message.getInt("errorClass");
 
                     final WebRequestError err = WebRequestError.fromGeckoError(errorCode, errorModule, errorClass);
 
@@ -582,77 +549,53 @@ public class GeckoSession implements Par
                         if (GeckoAppShell.isFennec()) {
                             callback.sendSuccess(null);
                         } else {
                             callback.sendError("abort");
                         }
                         return;
                     }
 
-                    result.then(new GeckoResult.OnValueListener<String, Void>() {
-                        @Override
-                        public GeckoResult<Void> onValue(final @Nullable String url)
-                                throws Throwable {
-                            if (url == null) {
-                                if (GeckoAppShell.isFennec()) {
-                                    callback.sendSuccess(null);
-                                } else {
-                                    callback.sendError("abort");
-                                }
+                    result.accept(url -> {
+                        if (url == null) {
+                            if (GeckoAppShell.isFennec()) {
+                                callback.sendSuccess(null);
                             } else {
-                                callback.sendSuccess(url);
+                                callback.sendError("abort");
                             }
-                            return null;
+                        } else {
+                            callback.sendSuccess(url);
                         }
-                    }, new GeckoResult.OnExceptionListener<Void>() {
-                        @Override
-                        public GeckoResult<Void> onException(final @NonNull Throwable exception) throws Throwable {
-                            callback.sendError(exception.getMessage());
-                            return null;
-                        }
-                    });
+                    }, exception -> callback.sendError(exception.getMessage()));
                 } else if ("GeckoView:OnNewSession".equals(event)) {
                     final String uri = message.getString("uri");
                     final GeckoResult<GeckoSession> result = delegate.onNewSession(GeckoSession.this, uri);
                     if (result == null) {
                         callback.sendSuccess(null);
                         return;
                     }
 
-                    result.then(new GeckoResult.OnValueListener<GeckoSession, Void>() {
-                        @Override
-                        public GeckoResult<Void> onValue(final GeckoSession session)
-                                throws Throwable {
-                            ThreadUtils.assertOnUiThread();
-                            if (session == null) {
-                                callback.sendSuccess(null);
-                                return null;
-                            }
-
-                            if (session.isOpen()) {
-                                throw new IllegalArgumentException("Must use an unopened GeckoSession instance");
-                            }
-
-                            if (GeckoSession.this.mWindow == null) {
-                                callback.sendError("Session is not attached to a window");
-                            } else {
-                                session.open(GeckoSession.this.mWindow.runtime);
-                                callback.sendSuccess(session.getId());
-                            }
-
-                            return null;
+                    result.accept(session -> {
+                        ThreadUtils.assertOnUiThread();
+                        if (session == null) {
+                            callback.sendSuccess(null);
+                            return;
                         }
-                    }, new GeckoResult.OnExceptionListener<Void>() {
-                        @Override
-                        public GeckoResult<Void> onException(final Throwable exception)
-                                throws Throwable {
-                            callback.sendError(exception.getMessage());
-                            return null;
+
+                        if (session.isOpen()) {
+                            throw new IllegalArgumentException("Must use an unopened GeckoSession instance");
                         }
-                    });
+
+                        if (GeckoSession.this.mWindow == null) {
+                            callback.sendError("Session is not attached to a window");
+                        } else {
+                            session.open(GeckoSession.this.mWindow.runtime);
+                            callback.sendSuccess(session.getId());
+                        }
+                    }, exception -> callback.sendError(exception.getMessage()));
                 }
             }
         };
 
     private final GeckoSessionHandler<ProgressDelegate> mProgressHandler =
         new GeckoSessionHandler<ProgressDelegate>(
             "GeckoViewProgress", this,
             new String[]{
@@ -2751,36 +2694,26 @@ public class GeckoSession implements Par
                 GeckoResult<AllowOrDeny> res = delegate.onPopupRequest(session, message.getString("targetUri"));
 
                 if (res == null) {
                     // Keep the popup blocked if the delegate returns null
                     callback.sendSuccess(false);
                     return;
                 }
 
-                res.then(new GeckoResult.OnValueListener<AllowOrDeny, Void>() {
-                    @Override
-                    public GeckoResult<Void> onValue(final AllowOrDeny value) throws Throwable {
-                        if (value == AllowOrDeny.ALLOW) {
-                            callback.sendSuccess(true);
-                        } else if (value == AllowOrDeny.DENY) {
-                            callback.sendSuccess(false);
-                        } else {
-                            callback.sendError("Invalid response");
-                        }
-                        return null;
+                res.accept(value -> {
+                    if (value == AllowOrDeny.ALLOW) {
+                        callback.sendSuccess(true);
+                    } else if (value == AllowOrDeny.DENY) {
+                        callback.sendSuccess(false);
+                    } else {
+                        callback.sendError("Invalid response");
                     }
-                }, new GeckoResult.OnExceptionListener<Void>() {
-                    @Override
-                    public GeckoResult<Void> onException(final Throwable exception)
-                            throws Throwable {
-                        callback.sendError("Failed to get popup-blocking decision");
-                        return null;
-                    }
-                });
+                }, exception ->
+                        callback.sendError("Failed to get popup-blocking decision"));
                 break;
             }
             default: {
                 callback.sendError("Invalid type");
                 break;
             }
         }
     }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionEventDispatcher.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionEventDispatcher.java
@@ -216,25 +216,19 @@ import java.util.Map;
         }
 
         final GeckoResult<Object> response = delegate.onMessage(content, sender);
         if (response == null) {
             callback.sendSuccess(null);
             return;
         }
 
-        response.then(
-            value -> {
-                callback.sendSuccess(value);
-                return null;
-            },
-            exception -> {
-                callback.sendError(exception);
-                return null;
-            });
+        response.accept(
+            value -> callback.sendSuccess(value),
+            exception -> callback.sendError(exception));
     }
 
     public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback, final GeckoSession session) {
         if ("GeckoView:WebExtension:Disconnect".equals(event)) {
             disconnect(message.getLong("portId", -1), callback);
             return;
         } else if ("GeckoView:WebExtension:PortMessage".equals(event)) {
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -444,29 +444,20 @@ public class GeckoViewActivity extends A
         for (GeckoSession.WebResponseInfo response : downloads) {
             downloadFile(response);
         }
     }
 
     private void downloadFile(GeckoSession.WebResponseInfo response) {
         mTabSessionManager.getCurrentSession()
                 .getUserAgent()
-                .then(new GeckoResult.OnValueListener<String, Void>() {
-            @Override
-            public GeckoResult<Void> onValue(String userAgent) throws Throwable {
-                downloadFile(response, userAgent);
-                return null;
-            }
-        }, new GeckoResult.OnExceptionListener<Void>() {
-            @Override
-            public GeckoResult<Void> onException(Throwable exception) throws Throwable {
-                // getUserAgent() cannot fail.
-                throw new IllegalStateException("Could not get UserAgent string.");
-            }
-        });
+                .accept(userAgent -> downloadFile(response, userAgent),
+                        exception -> {
+                    throw new IllegalStateException("Could not get UserAgent string.");
+                });
     }
 
     private void downloadFile(GeckoSession.WebResponseInfo response, String userAgent) {
         if (ContextCompat.checkSelfPermission(GeckoViewActivity.this,
                 Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
             mPendingDownloads.add(response);
             ActivityCompat.requestPermissions(GeckoViewActivity.this,
                     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},