Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.SimpleArrayMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeoutException;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;
import org.mozilla.gecko.util.IXPCOMEventTarget;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.XPCOMEventTarget;
/**
* 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;();
* (new Thread(() -&gt; {
* if (divisor != 0) {
* result.complete(dividend / divisor);
* } else {
* result.completeExceptionally(new ArithmeticException("Dividing by zero"));
* }
* })).start();
* return result;
* }</pre>
*
* <p>To retrieve the completed value or exception, use one of the {@link #then} methods to register
* listeners on the result. Listeners are run on the thread where the GeckoResult is created if a
* {@link Looper} is present. For example, to retrieve a completed value,
*
* <pre>
* divide(42, 2).then(new GeckoResult.OnValueListener&lt;Integer, Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onValue(final Integer value) {
* // value == 21
* }
* }, new GeckoResult.OnExceptionListener&lt;Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onException(final Throwable exception) {
* // Not called
* }
* });</pre>
*
* <p>And to retrieve a completed exception,
*
* <pre>
* divide(42, 0).then(new GeckoResult.OnValueListener&lt;Integer, Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onValue(final Integer value) {
* // Not called
* }
* }, new GeckoResult.OnExceptionListener&lt;Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onException(final Throwable exception) {
* // exception instanceof ArithmeticException
* }
* });</pre>
*
* <p>{@link #then} calls may be chained to complete multiple asynchonous operations in sequence.
* This example takes an integer, converts it to a String, and appends it to another String,
*
* <pre>
* divide(42, 2).then(new GeckoResult.OnValueListener&lt;Integer, String&gt;() {
* &#64;Override
* public GeckoResult&lt;String&gt; onValue(final Integer value) {
* return GeckoResult.fromValue(value.toString());
* }
* }).then(new GeckoResult.OnValueListener&lt;String, String&gt;() {
* &#64;Override
* public GeckoResult&lt;String&gt; onValue(final String value) {
* return GeckoResult.fromValue("42 / 2 = " + value);
* }
* }).then(new GeckoResult.OnValueListener&lt;String, Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onValue(final String value) {
* // value == "42 / 2 = 21"
* return null;
* }
* });</pre>
*
* <p>Chaining works with exception listeners as well. For example,
*
* <pre>
* divide(42, 0).then(new GeckoResult.OnExceptionListener&lt;String&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onException(final Throwable exception) {
* return "foo";
* }
* }).then(new GeckoResult.OnValueListener&lt;String, Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onValue(final String value) {
* // value == "foo"
* }
* });</pre>
*
* <p>A completed value/exception will propagate down the chain even if an intermediate step does
* not have a value/exception listener. For example,
*
* <pre>
* divide(42, 0).then(new GeckoResult.OnValueListener&lt;Integer, String&gt;() {
* &#64;Override
* public GeckoResult&lt;String&gt; onValue(final Integer value) {
* // Not called
* }
* }).then(new GeckoResult.OnExceptionListener&lt;Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onException(final Throwable exception) {
* // exception instanceof ArithmeticException
* }
* });</pre>
*
* <p>However, any propagated value will be coerced to null. For example,
*
* <pre>
* divide(42, 2).then(new GeckoResult.OnExceptionListener&lt;String&gt;() {
* &#64;Override
* public GeckoResult&lt;String&gt; onException(final Throwable exception) {
* // Not called
* }
* }).then(new GeckoResult.OnValueListener&lt;String, Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onValue(final String value) {
* // value == null
* }
* });</pre>
*
* <p>If a GeckoResult is created on a thread without a {@link Looper}, {@link
* #then(OnValueListener, OnExceptionListener)} is unusable (and will throw {@link
* IllegalThreadStateException}). In this scenario, the value is only available via {@link
* #poll(long)}. Alternatively, you may also chain the GeckoResult to one with a {@link Handler} via
* {@link #withHandler(Handler)}. You may then use {@link #then(OnValueListener,
* OnExceptionListener)} on the returned GeckoResult normally.
*
* <p>Any exception thrown by a listener are automatically used to complete the result. At the end
* of every chain, there is an implicit exception listener that rethrows any uncaught and unhandled
* exception as {@link UncaughtException}. The following example will cause {@link
* UncaughtException} to be thrown because {@code BazException} is uncaught and unhandled at the end
* of the chain,
*
* <pre>
* GeckoResult.fromValue(42).then(new GeckoResult.OnValueListener&lt;Integer, Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onValue(final Integer value) throws FooException {
* throw new FooException();
* }
* }).then(new GeckoResult.OnExceptionListener&lt;Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onException(final Throwable exception) throws Exception {
* // exception instanceof FooException
* throw new BarException();
* }
* }).then(new GeckoResult.OnExceptionListener&lt;Void&gt;() {
* &#64;Override
* public GeckoResult&lt;Void&gt; onException(final Throwable exception) throws Throwable {
* // exception instanceof BarException
* return new BazException();
* }
* });</pre>
*
* @param <T> The type of the value delivered via the GeckoResult.
*/
@AnyThread
public class GeckoResult<T> {
private static final String LOGTAG = "GeckoResult";
private interface Dispatcher {
void dispatch(Runnable r);
}
private static class HandlerDispatcher implements Dispatcher {
HandlerDispatcher(final Handler h) {
mHandler = h;
}
public void dispatch(final Runnable r) {
mHandler.post(r);
}
@Override
public boolean equals(final Object other) {
if (!(other instanceof HandlerDispatcher)) {
return false;
}
return mHandler.equals(((HandlerDispatcher) other).mHandler);
}
@Override
public int hashCode() {
return mHandler.hashCode();
}
Handler mHandler;
}
private static class XPCOMEventTargetDispatcher implements Dispatcher {
private IXPCOMEventTarget mEventTarget;
public XPCOMEventTargetDispatcher(final IXPCOMEventTarget eventTarget) {
mEventTarget = eventTarget;
}
@Override
public void dispatch(final Runnable r) {
mEventTarget.execute(r);
}
}
private static class DirectDispatcher implements Dispatcher {
public void dispatch(final Runnable r) {
r.run();
}
static DirectDispatcher sInstance = new DirectDispatcher();
private DirectDispatcher() {}
}
public static final class UncaughtException extends RuntimeException {
@SuppressWarnings("checkstyle:javadocmethod")
public UncaughtException(final Throwable cause) {
super(cause);
}
}
/** Interface used to delegate cancellation operations for a {@link GeckoResult}. */
@AnyThread
public interface CancellationDelegate {
/**
* This method should attempt to cancel the in-progress operation for the result to which this
* instance was attached. See {@link GeckoResult#cancel()} for more details.
*
* @return A {@link GeckoResult} resolving to "true" if cancellation was successful, "false"
* otherwise.
*/
default @NonNull GeckoResult<Boolean> cancel() {
return GeckoResult.fromValue(false);
}
}
/**
* @return a {@link GeckoResult} that resolves to {@link AllowOrDeny#DENY}
*/
@AnyThread
@NonNull
public static GeckoResult<AllowOrDeny> deny() {
return GeckoResult.fromValue(AllowOrDeny.DENY);
}
/**
* @return a {@link GeckoResult} that resolves to {@link AllowOrDeny#ALLOW}
*/
@AnyThread
@NonNull
public static GeckoResult<AllowOrDeny> allow() {
return GeckoResult.fromValue(AllowOrDeny.ALLOW);
}
// The default dispatcher for listeners on this GeckoResult. Other dispatchers can be specified
// when the listener is registered.
private final Dispatcher mDispatcher;
private boolean mComplete;
private T mValue;
private Throwable mError;
private boolean mIsUncaughtError;
private SimpleArrayMap<Dispatcher, ArrayList<Runnable>> mListeners = new SimpleArrayMap<>();
private GeckoResult<?> mParent;
private CancellationDelegate mCancellationDelegate;
/**
* Construct an incomplete GeckoResult. Call {@link #complete(Object)} or {@link
* #completeExceptionally(Throwable)} in order to fulfill the result.
*/
@WrapForJNI
public GeckoResult() {
if (ThreadUtils.isOnUiThread()) {
mDispatcher = new HandlerDispatcher(ThreadUtils.getUiHandler());
} else if (Looper.myLooper() != null) {
mDispatcher = new HandlerDispatcher(new Handler());
} else if (XPCOMEventTarget.launcherThread().isOnCurrentThread()) {
mDispatcher = new XPCOMEventTargetDispatcher(XPCOMEventTarget.launcherThread());
} else {
mDispatcher = null;
}
}
/**
* Construct an incomplete GeckoResult. Call {@link #complete(Object)} or {@link
* #completeExceptionally(Throwable)} in order to fulfill the result.
*
* @param handler This {@link Handler} will be used for dispatching listeners registered via
* {@link #then(OnValueListener, OnExceptionListener)}.
*/
public GeckoResult(final Handler handler) {
mDispatcher = new HandlerDispatcher(handler);
}
/**
* This constructs a result that is chained to the specified result.
*
* @param from The {@link GeckoResult} to copy.
*/
public GeckoResult(final GeckoResult<T> from) {
this();
completeFrom(from);
}
/**
* Construct a result that is completed with the specified value.
*
* @param value The value used to complete the newly created result.
* @param <U> Type for the result.
* @return The completed {@link GeckoResult}
*/
@WrapForJNI
public static @NonNull <U> GeckoResult<U> fromValue(@Nullable final U value) {
final GeckoResult<U> result = new GeckoResult<>();
result.complete(value);
return result;
}
/**
* Construct 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.
* @param <T> Type for the result if the result had been completed without exception.
* @return The completed {@link GeckoResult}
*/
@WrapForJNI
public static @NonNull <T> GeckoResult<T> fromException(@NonNull final Throwable error) {
final GeckoResult<T> result = new GeckoResult<>();
result.completeExceptionally(error);
return result;
}
@Override
public synchronized int hashCode() {
return Arrays.hashCode(new Object[] {mComplete, mValue, mError});
}
// 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 synchronized 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> 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> then(@NonNull final OnValueListener<T, U> valueListener) {
return then(valueListener, null);
}
/**
* Convenience method for {@link #map(OnValueMapper, OnExceptionMapper)}.
*
* @param valueMapper An instance of {@link OnValueMapper}, called when the {@link GeckoResult} is
* completed with a value.
* @param <U> Type of the new value that is returned by the mapper.
* @return A new {@link GeckoResult} that will contain the mapped value.
*/
public @NonNull <U> GeckoResult<U> map(@Nullable final OnValueMapper<T, U> valueMapper) {
return map(valueMapper, null);
}
/**
* Transform the value and error of this {@link GeckoResult}.
*
* @param valueMapper An instance of {@link OnValueMapper}, called when the {@link GeckoResult} is
* completed with a value.
* @param exceptionMapper An instance of {@link OnExceptionMapper}, called when the {@link
* GeckoResult} is completed with an exception.
* @param <U> Type of the new value that is returned by the mapper.
* @return A new {@link GeckoResult} that will contain the mapped value.
*/
public @NonNull <U> GeckoResult<U> map(
@Nullable final OnValueMapper<T, U> valueMapper,
@Nullable final OnExceptionMapper exceptionMapper) {
final OnValueListener<T, U> valueListener =
valueMapper != null ? value -> GeckoResult.fromValue(valueMapper.onValue(value)) : null;
final OnExceptionListener<U> exceptionListener =
exceptionMapper != null
? error -> GeckoResult.fromException(exceptionMapper.onException(error))
: null;
return then(valueListener, exceptionListener);
}
/**
* 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> 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.
*/
@AnyThread
void accept(@Nullable 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}.
*
* <p>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 regardless of success
* status. Listeners will be invoked on the {@link Looper} returned from {@link #getLooper()}. If
* null, this method will throw {@link IllegalThreadStateException}.
*
* <p>If the result is already complete when this method is called, listeners will be invoked in a
* future {@link Looper} iteration.
*
* @param finallyRunnable An instance of {@link Runnable}, called when the {@link GeckoResult} is
* completed with a value or a {@link Throwable}.
* @return A new {@link GeckoResult} that the listeners will complete.
*/
public @NonNull GeckoResult<Void> finally_(@NonNull final Runnable finallyRunnable) {
final OnValueListener<T, Void> valueListener =
value -> {
finallyRunnable.run();
return null;
};
final OnExceptionListener<Void> exceptionListener =
value -> {
finallyRunnable.run();
return null;
};
return then(valueListener, exceptionListener);
}
/* package */ @NonNull
GeckoResult<Void> getOrAccept(@Nullable final Consumer<T> valueConsumer) {
return getOrAccept(valueConsumer, null);
}
/* package */ @NonNull
GeckoResult<Void> getOrAccept(
@Nullable final Consumer<T> valueConsumer,
@Nullable final Consumer<Throwable> exceptionConsumer) {
if (haveValue() && valueConsumer != null) {
valueConsumer.accept(mValue);
return GeckoResult.fromValue(null);
}
if (haveError() && exceptionConsumer != null) {
exceptionConsumer.accept(mError);
return GeckoResult.fromValue(null);
}
return accept(valueConsumer, exceptionConsumer);
}
/**
* 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}.
*
* <p>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> Type of the new result that is returned by the listeners.
* @return A new {@link GeckoResult} that the listeners will complete.
*/
public @NonNull <U> GeckoResult<U> then(
@Nullable final OnValueListener<T, U> valueListener,
@Nullable final OnExceptionListener<U> exceptionListener) {
if (mDispatcher == null) {
throw new IllegalThreadStateException("Must have a Handler");
}
return thenInternal(mDispatcher, valueListener, exceptionListener);
}
private @NonNull <U> GeckoResult<U> thenInternal(
@NonNull final Dispatcher dispatcher,
@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>();
result.mParent = this;
thenInternal(
dispatcher,
() -> {
try {
if (haveValue()) {
result.completeFrom(valueListener != null ? valueListener.onValue(mValue) : null);
} else if (!haveError()) {
// Listener called without completion?
throw new AssertionError();
} else if (exceptionListener != null) {
result.completeFrom(exceptionListener.onException(mError));
} else {
result.mIsUncaughtError = mIsUncaughtError;
result.completeExceptionally(mError);
}
} catch (final Throwable e) {
if (!result.mComplete) {
result.mIsUncaughtError = true;
result.completeExceptionally(e);
} else if (e instanceof RuntimeException) {
// This should only be UncaughtException, but we rethrow all RuntimeExceptions
// to avoid squelching logic errors in GeckoResult itself.
throw (RuntimeException) e;
}
}
});
return result;
}
private synchronized void thenInternal(
@NonNull final Dispatcher dispatcher, @NonNull final Runnable listener) {
if (mComplete) {
dispatcher.dispatch(listener);
} else {
if (!mListeners.containsKey(dispatcher)) {
mListeners.put(dispatcher, new ArrayList<>(1));
}
mListeners.get(dispatcher).add(listener);
}
}
@WrapForJNI
private void nativeThen(
@NonNull final GeckoCallback accept, @NonNull final GeckoCallback reject) {
// NB: We could use the lambda syntax here, but given all the layers
// of abstraction it's helpful to see the types written explicitly.
thenInternal(
DirectDispatcher.sInstance,
new OnValueListener<T, Void>() {
@Override
public GeckoResult<Void> onValue(final T value) {
accept.call(value);
return null;
}
},
new OnExceptionListener<Void>() {
@Override
public GeckoResult<Void> onException(final Throwable exception) {
reject.call(exception);
return null;
}
});
}
/**
* @return Get the {@link Looper} that will be used to schedule listeners registered via {@link
* #then(OnValueListener, OnExceptionListener)}.
*/
public @Nullable Looper getLooper() {
if (mDispatcher == null || !(mDispatcher instanceof HandlerDispatcher)) {
return null;
}
return ((HandlerDispatcher) mDispatcher).mHandler.getLooper();
}
/**
* Returns a new GeckoResult that will be completed by this instance. Listeners registered via
* {@link #then(OnValueListener, OnExceptionListener)} will be run on the specified {@link
* Handler}.
*
* @param handler A {@link Handler} where listeners will be run. May be null.
* @return A new GeckoResult.
*/
public @NonNull GeckoResult<T> withHandler(final @Nullable Handler handler) {
final GeckoResult<T> result = new GeckoResult<>(handler);
result.completeFrom(this);
return result;
}
/**
* Returns a {@link GeckoResult} that is completed when the given {@link GeckoResult} instances
* are complete.
*
* <p>The returned {@link GeckoResult} will resolve with the list of values from the inputs. The
* list is guaranteed to be in the same order as the inputs.
*
* <p>If any of the {@link GeckoResult} fails, the returned result will fail.
*
* <p>If no inputs are provided, the returned {@link GeckoResult} will complete with the value
* <code>null</code>.
*
* @param pending the input {@link GeckoResult}s.
* @param <V> type of the {@link GeckoResult}'s values.
* @return a {@link GeckoResult} that will complete when all of the inputs are completed or when
* at least one of the inputs fail.
*/
@SuppressWarnings("varargs")
@SafeVarargs
@NonNull
public static <V> GeckoResult<List<V>> allOf(final @NonNull GeckoResult<V>... pending) {
return allOf(Arrays.asList(pending));
}
/**
* Returns a {@link GeckoResult} that is completed when the given {@link GeckoResult} instances
* are complete.
*
* <p>The returned {@link GeckoResult} will resolve with the list of values from the inputs. The
* list is guaranteed to be in the same order as the inputs.
*
* <p>If any of the {@link GeckoResult} fails, the returned result will fail.
*
* <p>If no inputs are provided, the returned {@link GeckoResult} will complete with the value
* <code>null</code>.
*
* @param pending the input {@link GeckoResult}s.
* @param <V> type of the {@link GeckoResult}'s values.
* @return a {@link GeckoResult} that will complete when all of the inputs are completed or when
* at least one of the inputs fail.
*/
@NonNull
public static <V> GeckoResult<List<V>> allOf(final @Nullable List<GeckoResult<V>> pending) {
if (pending == null) {
return GeckoResult.fromValue(null);
}
return new AllOfResult<>(pending);
}
private static class AllOfResult<V> extends GeckoResult<List<V>> {
private boolean mFailed = false;
private int mResultCount = 0;
private final List<V> mAccumulator;
private final List<GeckoResult<V>> mPending;
public AllOfResult(final @NonNull List<GeckoResult<V>> pending) {
// Initialize the list with nulls so we can fill it in the same order as the input list
mAccumulator = new ArrayList<>(Collections.nCopies(pending.size(), null));
mPending = pending;
// If the input list is empty, there's nothing to do
if (pending.size() == 0) {
complete(mAccumulator);
return;
}
// We use iterators so we can access the index and preserve the list order
final ListIterator<GeckoResult<V>> it = pending.listIterator();
while (it.hasNext()) {
final int index = it.nextIndex();
it.next().accept(value -> onResult(value, index), this::onError);
}
}
private void onResult(final V value, final int index) {
if (mFailed) {
// Some other element in the list already failed, nothing to do here
return;
}
mResultCount++;
mAccumulator.set(index, value);
if (mResultCount == mPending.size()) {
complete(mAccumulator);
}
}
private void onError(final Throwable error) {
mFailed = true;
completeExceptionally(error);
}
}
private void dispatchLocked() {
if (!mComplete) {
throw new IllegalStateException("Cannot dispatch unless result is complete");
}
if (mListeners.isEmpty()) {
if (mIsUncaughtError) {
// We have no listeners to forward the uncaught exception to;
// rethrow the exception to make it visible.
throw new UncaughtException(mError);
}
return;
}
if (mDispatcher == null) {
throw new AssertionError("Shouldn't have listeners with null dispatcher");
}
for (int i = 0; i < mListeners.size(); ++i) {
final Dispatcher dispatcher = mListeners.keyAt(i);
final ArrayList<Runnable> jobs = mListeners.valueAt(i);
dispatcher.dispatch(
() -> {
for (final Runnable job : jobs) {
job.run();
}
});
}
mListeners.clear();
}
/**
* Completes this result based on another result.
*
* @param other The result that this result should mirror
*/
public void completeFrom(final @Nullable GeckoResult<T> other) {
if (other == null) {
complete(null);
return;
}
this.mCancellationDelegate = other.mCancellationDelegate;
other.thenInternal(
DirectDispatcher.sInstance,
() -> {
if (other.haveValue()) {
complete(other.mValue);
} else {
mIsUncaughtError = other.mIsUncaughtError;
completeExceptionally(other.mError);
}
});
}
/**
* Return the value of this result, waiting for it to be completed if necessary. If the result is
* completed with an exception it will be rethrown here.
*
* <p>You must not call this method if the current thread has a {@link Looper} due to the
* possibility of a deadlock. If this occurs, {@link IllegalStateException} is thrown.
*
* @return The value of this result.
* @throws Throwable The {@link Throwable} contained in this result, if any.
* @throws IllegalThreadStateException if this method is called on a thread that has a {@link
* Looper}.
*/
public synchronized @Nullable T poll() throws Throwable {
if (Looper.myLooper() != null) {
throw new IllegalThreadStateException("Cannot poll indefinitely from thread with Looper");
}
return poll(Long.MAX_VALUE);
}
/**
* Return the value of this result, waiting for it to be completed if necessary. If the result is
* completed with an exception it will be rethrown here.
*
* <p>Caution is advised if the caller is on a thread with a {@link Looper}, as it's possible to
* effectively deadlock in cases when the work is being completed on the calling thread. It's
* preferable to use {@link #then(OnValueListener, OnExceptionListener)} in such circumstances,
* but if you must use this method consider a small timeout value.
*
* @param timeoutMillis Number of milliseconds to wait for the result to complete.
* @return The value of this result.
* @throws Throwable The {@link Throwable} contained in this result, if any.
* @throws TimeoutException if we wait more than timeoutMillis before the result is completed.
*/
public synchronized @Nullable T poll(final long timeoutMillis) throws Throwable {
final long start = SystemClock.uptimeMillis();
long remaining = timeoutMillis;
while (!mComplete && remaining > 0) {
try {
wait(remaining);
} catch (final InterruptedException e) {
}
remaining = timeoutMillis - (SystemClock.uptimeMillis() - start);
}
if (!mComplete) {
throw new TimeoutException();
}
if (haveError()) {
throw mError;
}
return mValue;
}
/**
* Complete 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 If the result is already completed.
*/
@WrapForJNI
public synchronized void complete(final @Nullable T value) {
if (mComplete) {
throw new IllegalStateException("result is already complete");
}
mValue = value;
mComplete = true;
dispatchLocked();
notifyAll();
}
/**
* Complete 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 If the result is already completed.
*/
@WrapForJNI
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;
dispatchLocked();
notifyAll();
}
/**
* An interface used to deliver values to listeners of a {@link GeckoResult}
*
* @param <T> Type of the value delivered via {@link #onValue(Object)}
* @param <U> 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. Will be called on the same
* thread where the GeckoResult was created or on the {@link Handler} provided via {@link
* #withHandler(Handler)}.
*
* @param value The value of the {@link GeckoResult}
* @return Result used to complete the next result in the chain. May be null.
* @throws Throwable Exception used to complete next result in the chain.
*/
@AnyThread
@Nullable
GeckoResult<U> onValue(@Nullable T value) throws Throwable;
}
/**
* An interface used to map {@link GeckoResult} values.
*
* @param <T> Type of the value delivered via {@link #onValue}
* @param <U> Type of the new value returned by {@link #onValue}
*/
public interface OnValueMapper<T, U> {
/**
* Called when a {@link GeckoResult} is completed with a value. Will be called on the same
* thread where the GeckoResult was created or on the {@link Handler} provided via {@link
* #withHandler(Handler)}.
*
* @param value The value of the {@link GeckoResult}
* @return Value used to complete the next result in the chain. May be null.
* @throws Throwable Exception used to complete next result in the chain.
*/
@AnyThread
@Nullable
U onValue(@Nullable T value) throws Throwable;
}
/** An interface used to map {@link GeckoResult} exceptions. */
public interface OnExceptionMapper {
/**
* Called when a {@link GeckoResult} is completed with an exception. Will be called on the same
* thread where the GeckoResult was created or on the {@link Handler} provided via {@link
* #withHandler(Handler)}.
*
* @param exception Exception that completed the result.
* @return Exception used to complete the next result in the chain. May be null.
* @throws Throwable Exception used to complete next result in the chain.
*/
@AnyThread
@Nullable
Throwable onException(@NonNull Throwable exception) throws Throwable;
}
/**
* An interface used to deliver exceptions to listeners of a {@link GeckoResult}
*
* @param <V> Type of the vale for the result returned from {@link #onException(Throwable)}
*/
public interface OnExceptionListener<V> {
/**
* Called when a {@link GeckoResult} is completed with an exception. Will be called on the same
* thread where the GeckoResult was created or on the {@link Handler} provided via {@link
* #withHandler(Handler)}.
*
* @param exception Exception that completed the result.
* @return Result used to complete the next result in the chain. May be null.
* @throws Throwable Exception used to complete next result in the chain.
*/
@AnyThread
@Nullable
GeckoResult<V> onException(@NonNull Throwable exception) throws Throwable;
}
@WrapForJNI
private static class GeckoCallback extends JNIObject {
private native void call(Object arg);
@Override
protected native void disposeNative();
}
private boolean haveValue() {
return mComplete && mError == null;
}
private boolean haveError() {
return mComplete && mError != null;
}
/**
* Attempts to cancel the operation associated with this result.
*
* <p>If this result has a {@link CancellationDelegate} attached via {@link
* #setCancellationDelegate(CancellationDelegate)}, the return value will be the result of calling
* {@link CancellationDelegate#cancel()} on that instance. Otherwise, if this result is chained to
* another result (via return value from {@link OnValueListener}), we will walk up the chain until
* a CancellationDelegate is found and run it. If no CancellationDelegate is found, a result
* resolving to "false" will be returned.
*
* <p>If this result is already complete, the returned result will always resolve to false.
*
* <p>If the returned result resolves to true, this result will be completed with a {@link
* CancellationException}.
*
* @return A GeckoResult resolving to a boolean indicating success or failure of the cancellation
* attempt.
*/
public synchronized @NonNull GeckoResult<Boolean> cancel() {
if (haveValue() || haveError()) {
return GeckoResult.fromValue(false);
}
if (mCancellationDelegate != null) {
return mCancellationDelegate
.cancel()
.then(
value -> {
if (value) {
try {
this.completeExceptionally(new CancellationException());
} catch (final IllegalStateException e) {
// Can't really do anything about this.
}
}
return GeckoResult.fromValue(value);
});
}
if (mParent != null) {
return mParent.cancel();
}
return GeckoResult.fromValue(false);
}
/**
* Sets the instance of {@link CancellationDelegate} that will be invoked by {@link #cancel()}.
*
* @param delegate an instance of CancellationDelegate.
*/
public void setCancellationDelegate(final @Nullable CancellationDelegate delegate) {
mCancellationDelegate = delegate;
}
}