Use CompletableFuture instead

This CL converts our inhouse Completable class with CompletableFuture.

One of downsides of switching into CompletableFuture is performance.
It creates much more objects, especially in unsuccessful cases
including timeout scenarios:

 * CompletableFuture#cancel() always creates a CancellationException
   object with full stack trace.  This is going to be problematic when
   we start cancelling pending InputConnection tasks in Bug 195115071
   from the IME client side.
 * Timeout cases always creates a TimeoutException object with a full
   stack trace.
 * Exception cases always creates a ExecutionException object with
   full stack trace.

Also, none of its getter methods directly fits our existing use cases
in the input method framework world.  We must always use
CompletableFutureUtil to retrieve the result value so as not to
accidentally break existing APIs.

Other than above, there should be no observable semantic behavior
changes in this CL.

Bug: 192412909
Bug: 195699814
Test: presubmit
Test: atest -c FrameworksCoreTests:CompletableFutureUtilTest
Change-Id: I215bbc870f952effa262fa431064b36ace28e8f4
This commit is contained in:
Yohei Yukawa
2021-08-29 13:28:40 -07:00
parent 8471046411
commit ba8cdf5dcf
8 changed files with 739 additions and 720 deletions

View File

@@ -34,7 +34,7 @@ import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.SurroundingText;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.Completable;
import com.android.internal.inputmethod.CompletableFutureUtil;
import com.android.internal.inputmethod.IInputContextInvoker;
import com.android.internal.inputmethod.ImeTracing;
import com.android.internal.inputmethod.InputConnectionProtoDumper;
@@ -42,6 +42,7 @@ import com.android.internal.view.IInputContext;
import com.android.internal.view.IInputMethod;
import java.lang.ref.WeakReference;
import java.util.concurrent.CompletableFuture;
/**
* Takes care of remote method invocations of {@link InputConnection} in the IME side.
@@ -96,8 +97,8 @@ final class RemoteInputConnection implements InputConnection {
return null;
}
final Completable.CharSequence value = mInvoker.getTextAfterCursor(length, flags);
final CharSequence result = Completable.getResultOrNull(
final CompletableFuture<CharSequence> value = mInvoker.getTextAfterCursor(length, flags);
final CharSequence result = CompletableFutureUtil.getResultOrNull(
value, TAG, "getTextAfterCursor()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
final InputMethodServiceInternal inputMethodService = mInputMethodService.get();
@@ -120,8 +121,8 @@ final class RemoteInputConnection implements InputConnection {
return null;
}
final Completable.CharSequence value = mInvoker.getTextBeforeCursor(length, flags);
final CharSequence result = Completable.getResultOrNull(
final CompletableFuture<CharSequence> value = mInvoker.getTextBeforeCursor(length, flags);
final CharSequence result = CompletableFutureUtil.getResultOrNull(
value, TAG, "getTextBeforeCursor()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
final InputMethodServiceInternal inputMethodService = mInputMethodService.get();
@@ -144,8 +145,8 @@ final class RemoteInputConnection implements InputConnection {
// This method is not implemented.
return null;
}
final Completable.CharSequence value = mInvoker.getSelectedText(flags);
final CharSequence result = Completable.getResultOrNull(
final CompletableFuture<CharSequence> value = mInvoker.getSelectedText(flags);
final CharSequence result = CompletableFutureUtil.getResultOrNull(
value, TAG, "getSelectedText()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
final InputMethodServiceInternal inputMethodService = mInputMethodService.get();
@@ -181,9 +182,9 @@ final class RemoteInputConnection implements InputConnection {
// This method is not implemented.
return null;
}
final Completable.SurroundingText value = mInvoker.getSurroundingText(beforeLength,
final CompletableFuture<SurroundingText> value = mInvoker.getSurroundingText(beforeLength,
afterLength, flags);
final SurroundingText result = Completable.getResultOrNull(
final SurroundingText result = CompletableFutureUtil.getResultOrNull(
value, TAG, "getSurroundingText()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
final InputMethodServiceInternal inputMethodService = mInputMethodService.get();
@@ -202,8 +203,8 @@ final class RemoteInputConnection implements InputConnection {
return 0;
}
final Completable.Int value = mInvoker.getCursorCapsMode(reqModes);
final int result = Completable.getResultOrZero(
final CompletableFuture<Integer> value = mInvoker.getCursorCapsMode(reqModes);
final int result = CompletableFutureUtil.getResultOrZero(
value, TAG, "getCursorCapsMode()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
final InputMethodServiceInternal inputMethodService = mInputMethodService.get();
@@ -222,8 +223,8 @@ final class RemoteInputConnection implements InputConnection {
return null;
}
final Completable.ExtractedText value = mInvoker.getExtractedText(request, flags);
final ExtractedText result = Completable.getResultOrNull(
final CompletableFuture<ExtractedText> value = mInvoker.getExtractedText(request, flags);
final ExtractedText result = CompletableFutureUtil.getResultOrNull(
value, TAG, "getExtractedText()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
final InputMethodServiceInternal inputMethodService = mInputMethodService.get();
@@ -371,8 +372,8 @@ final class RemoteInputConnection implements InputConnection {
// This method is not implemented.
return false;
}
final Completable.Boolean value = mInvoker.requestCursorUpdates(cursorUpdateMode);
return Completable.getResultOrFalse(value, TAG, "requestCursorUpdates()",
final CompletableFuture<Boolean> value = mInvoker.requestCursorUpdates(cursorUpdateMode);
return CompletableFutureUtil.getResultOrFalse(value, TAG, "requestCursorUpdates()",
mCancellationGroup, MAX_WAIT_TIME_MILLIS);
}
@@ -407,8 +408,9 @@ final class RemoteInputConnection implements InputConnection {
inputMethodService.exposeContent(inputContentInfo, this);
}
final Completable.Boolean value = mInvoker.commitContent(inputContentInfo, flags, opts);
return Completable.getResultOrFalse(
final CompletableFuture<Boolean> value =
mInvoker.commitContent(inputContentInfo, flags, opts);
return CompletableFutureUtil.getResultOrFalse(
value, TAG, "commitContent()", mCancellationGroup, MAX_WAIT_TIME_MILLIS);
}

View File

@@ -23,49 +23,66 @@ import android.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CompletableFuture;
/**
* A utility class, which works as both a factory class of a cancellation signal to cancel
* all the completable objects.
*
* <p>TODO: Make this lock-free.</p>
*/
public final class CancellationGroup {
private final Object mLock = new Object();
/**
* List of {@link CountDownLatch}, which can be used to propagate {@link #cancelAll()} to
* List of {@link CompletableFuture}, which can be used to propagate {@link #cancelAll()} to
* completable objects.
*
* <p>This will be lazily instantiated to avoid unnecessary object allocations.</p>
*/
@Nullable
@GuardedBy("mLock")
private ArrayList<CountDownLatch> mLatchList = null;
private ArrayList<CompletableFuture<?>> mFutureList = null;
@GuardedBy("mLock")
private boolean mCanceled = false;
/**
* Tries to register the given {@link CompletableFuture} into the callback list if this
* {@link CancellationGroup} is not yet cancelled.
*
* <p>If this {@link CancellationGroup} is already cancelled, then this method will immediately
* call {@link CompletableFuture#cancel(boolean)} then return {@code false}.</p>
*
* <p>When this method returns {@code true}, call {@link #unregisterFuture(CompletableFuture)}
* to remove the unnecessary object reference.</p>
*
* @param future {@link CompletableFuture} to be added to the cancellation callback list.
* @return {@code true} if the given {@code future} is added to the callback list.
* {@code false} otherwise.
*/
@AnyThread
boolean registerLatch(@NonNull CountDownLatch latch) {
boolean tryRegisterFutureOrCancelImmediately(@NonNull CompletableFuture<?> future) {
synchronized (mLock) {
if (mCanceled) {
future.cancel(false);
return false;
}
if (mLatchList == null) {
if (mFutureList == null) {
// Set the initial capacity to 1 with an assumption that usually there is up to 1
// on-going operation.
mLatchList = new ArrayList<>(1);
mFutureList = new ArrayList<>(1);
}
mLatchList.add(latch);
mFutureList.add(future);
return true;
}
}
@AnyThread
void unregisterLatch(@NonNull CountDownLatch latch) {
void unregisterFuture(@NonNull CompletableFuture<?> future) {
synchronized (mLock) {
if (mLatchList != null) {
mLatchList.remove(latch);
if (mFutureList != null) {
mFutureList.remove(future);
}
}
}
@@ -80,10 +97,10 @@ public final class CancellationGroup {
synchronized (mLock) {
if (!mCanceled) {
mCanceled = true;
if (mLatchList != null) {
mLatchList.forEach(CountDownLatch::countDown);
mLatchList.clear();
mLatchList = null;
if (mFutureList != null) {
mFutureList.forEach(future -> future.cancel(false));
mFutureList.clear();
mFutureList = null;
}
}
}

View File

@@ -1,558 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.inputmethod;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.AnyThread;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.lang.annotation.Retention;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* An class to consolidate completable object types supported by
* {@link CancellationGroup}.
*/
public final class Completable {
/**
* Not intended to be instantiated.
*/
private Completable() {
}
/**
* Base class of all the completable types supported by {@link CancellationGroup}.
*/
protected static class ValueBase {
/**
* {@link CountDownLatch} to be signaled to unblock
* {@link #await(int, TimeUnit, CancellationGroup)}.
*/
private final CountDownLatch mLatch = new CountDownLatch(1);
/**
* Lock {@link Object} to guard complete operations within this class.
*/
protected final Object mStateLock = new Object();
/**
* Indicates the completion state of this object.
*/
@GuardedBy("mStateLock")
@CompletionState
protected int mState = CompletionState.NOT_COMPLETED;
/**
* {@link Throwable} message passed to {@link #onError(ThrowableHolder)}.
*
* <p>This is not {@code null} only when {@link #mState} is
* {@link CompletionState#COMPLETED_WITH_ERROR}.</p>
*/
@GuardedBy("mStateLock")
@Nullable
protected String mMessage = null;
@Retention(SOURCE)
@IntDef({
CompletionState.NOT_COMPLETED,
CompletionState.COMPLETED_WITH_VALUE,
CompletionState.COMPLETED_WITH_ERROR})
protected @interface CompletionState {
/**
* This object is not completed yet.
*/
int NOT_COMPLETED = 0;
/**
* This object is already completed with a value.
*/
int COMPLETED_WITH_VALUE = 1;
/**
* This object is already completed with an error.
*/
int COMPLETED_WITH_ERROR = 2;
}
/**
* Converts the given {@link CompletionState} into a human-readable string.
*
* @param state {@link CompletionState} to be converted.
* @return a human-readable {@link String} for the given {@code state}.
*/
@AnyThread
protected static String stateToString(@CompletionState int state) {
switch (state) {
case CompletionState.NOT_COMPLETED:
return "NOT_COMPLETED";
case CompletionState.COMPLETED_WITH_VALUE:
return "COMPLETED_WITH_VALUE";
case CompletionState.COMPLETED_WITH_ERROR:
return "COMPLETED_WITH_ERROR";
default:
return "Unknown(value=" + state + ")";
}
}
/**
* @return {@code true} if {@link #onComplete()} gets called and {@link #mState} is
* {@link CompletionState#COMPLETED_WITH_VALUE}.
*/
@AnyThread
public boolean hasValue() {
synchronized (mStateLock) {
return mState == CompletionState.COMPLETED_WITH_VALUE;
}
}
/**
* Provides the base implementation of {@code getValue()} for derived classes.
*
* <p>Must be called after acquiring {@link #mStateLock}.</p>
*
* @throws RuntimeException when {@link #mState} is
* {@link CompletionState#COMPLETED_WITH_ERROR}.
* @throws UnsupportedOperationException when {@link #mState} is not
* {@link CompletionState#COMPLETED_WITH_VALUE} and
* {@link CompletionState#COMPLETED_WITH_ERROR}.
*/
@GuardedBy("mStateLock")
protected void enforceGetValueLocked() {
switch (mState) {
case CompletionState.NOT_COMPLETED:
throw new UnsupportedOperationException(
"getValue() is allowed only if hasValue() returns true");
case CompletionState.COMPLETED_WITH_VALUE:
return;
case CompletionState.COMPLETED_WITH_ERROR:
throw new RuntimeException(mMessage);
default:
throw new UnsupportedOperationException(
"getValue() is not allowed on state=" + stateToString(mState));
}
}
/**
* Called by subclasses to signale {@link #mLatch}.
*/
@AnyThread
protected void onComplete() {
mLatch.countDown();
}
/**
* Notify when exception happened.
*
* @param throwableHolder contains the {@link Throwable} object when exception happened.
*/
@AnyThread
protected void onError(ThrowableHolder throwableHolder) {
synchronized (mStateLock) {
switch (mState) {
case CompletionState.NOT_COMPLETED:
mMessage = throwableHolder.getMessage();
mState = CompletionState.COMPLETED_WITH_ERROR;
break;
default:
throw new UnsupportedOperationException(
"onError() is not allowed on state=" + stateToString(mState));
}
}
onComplete();
}
/**
* Blocks the calling thread until at least one of the following conditions is met.
*
* <p>
* <ol>
* <li>This object becomes ready to return the value.</li>
* <li>{@link CancellationGroup#cancelAll()} gets called.</li>
* <li>The given timeout period has passed.</li>
* </ol>
* </p>
*
* <p>The caller can distinguish the case 1 and case 2 by calling {@link #hasValue()}.
* Note that the return value of {@link #hasValue()} can change from {@code false} to
* {@code true} at any time, even after this methods finishes with returning
* {@code true}.</p>
*
* @param timeout length of the timeout.
* @param timeUnit unit of {@code timeout}.
* @param cancellationGroup {@link CancellationGroup} to cancel completable objects.
* @return {@code false} if and only if the given timeout period has passed. Otherwise
* {@code true}.
*/
@AnyThread
public boolean await(int timeout, @NonNull TimeUnit timeUnit,
@Nullable CancellationGroup cancellationGroup) {
if (cancellationGroup == null) {
return awaitInner(timeout, timeUnit);
}
if (!cancellationGroup.registerLatch(mLatch)) {
// Already canceled when this method gets called.
return false;
}
try {
return awaitInner(timeout, timeUnit);
} finally {
cancellationGroup.unregisterLatch(mLatch);
}
}
private boolean awaitInner(int timeout, @NonNull TimeUnit timeUnit) {
try {
return mLatch.await(timeout, timeUnit);
} catch (InterruptedException e) {
return true;
}
}
/**
* Blocks the calling thread until this object becomes ready to return the value, even if
* {@link InterruptedException} is thrown.
*/
@AnyThread
public void await() {
boolean interrupted = false;
while (true) {
try {
mLatch.await();
break;
} catch (InterruptedException ignored) {
interrupted = true;
}
}
if (interrupted) {
// Try to preserve the interrupt bit on this thread.
Thread.currentThread().interrupt();
}
}
}
/**
* Completable object of integer primitive.
*/
public static final class Int extends ValueBase {
@GuardedBy("mStateLock")
private int mValue = 0;
/**
* Notify when a value is set to this completable object.
*
* @param value value to be set.
*/
@AnyThread
void onComplete(int value) {
synchronized (mStateLock) {
switch (mState) {
case CompletionState.NOT_COMPLETED:
mValue = value;
mState = CompletionState.COMPLETED_WITH_VALUE;
break;
default:
throw new UnsupportedOperationException(
"onComplete() is not allowed on state=" + stateToString(mState));
}
}
onComplete();
}
/**
* @return value associated with this object.
* @throws RuntimeException when called while {@link #onError} happened.
* @throws UnsupportedOperationException when called while {@link #hasValue()} returns
* {@code false}.
*/
@AnyThread
public int getValue() {
synchronized (mStateLock) {
enforceGetValueLocked();
return mValue;
}
}
}
/**
* Completable object of {@link java.lang.Void}.
*/
public static final class Void extends ValueBase {
/**
* Notify when this completable object callback.
*/
@AnyThread
@Override
protected void onComplete() {
synchronized (mStateLock) {
switch (mState) {
case CompletionState.NOT_COMPLETED:
mState = CompletionState.COMPLETED_WITH_VALUE;
break;
default:
throw new UnsupportedOperationException(
"onComplete() is not allowed on state=" + stateToString(mState));
}
}
super.onComplete();
}
/**
* @throws RuntimeException when called while {@link #onError} happened.
* @throws UnsupportedOperationException when called while {@link #hasValue()} returns
* {@code false}.
*/
@AnyThread
public void getValue() {
synchronized (mStateLock) {
enforceGetValueLocked();
}
}
}
/**
* Base class of completable object types.
*
* @param <T> type associated with this completable object.
*/
public static class Values<T> extends ValueBase {
@GuardedBy("mStateLock")
@Nullable
private T mValue = null;
/**
* Notify when a value is set to this completable value object.
*
* @param value value to be set.
*/
@AnyThread
void onComplete(@Nullable T value) {
synchronized (mStateLock) {
switch (mState) {
case CompletionState.NOT_COMPLETED:
mValue = value;
mState = CompletionState.COMPLETED_WITH_VALUE;
break;
default:
throw new UnsupportedOperationException(
"onComplete() is not allowed on state=" + stateToString(mState));
}
}
onComplete();
}
/**
* @return value associated with this object.
* @throws RuntimeException when called while {@link #onError} happened
* @throws UnsupportedOperationException when called while {@link #hasValue()} returns
* {@code false}.
*/
@AnyThread
@Nullable
public T getValue() {
synchronized (mStateLock) {
enforceGetValueLocked();
return mValue;
}
}
}
/**
* @return an instance of {@link Completable.Int}.
*/
public static Completable.Int createInt() {
return new Completable.Int();
}
/**
* @return an instance of {@link Completable.Boolean}.
*/
public static Completable.Boolean createBoolean() {
return new Completable.Boolean();
}
/**
* @return an instance of {@link Completable.CharSequence}.
*/
public static Completable.CharSequence createCharSequence() {
return new Completable.CharSequence();
}
/**
* @return an instance of {@link Completable.ExtractedText}.
*/
public static Completable.ExtractedText createExtractedText() {
return new Completable.ExtractedText();
}
/**
* @return an instance of {@link Completable.SurroundingText}.
*/
public static Completable.SurroundingText createSurroundingText() {
return new Completable.SurroundingText();
}
/**
* @return an instance of {@link Completable.IInputContentUriToken}.
*/
public static Completable.IInputContentUriToken createIInputContentUriToken() {
return new Completable.IInputContentUriToken();
}
/**
* @return an instance of {@link Completable.Void}.
*/
public static Completable.Void createVoid() {
return new Completable.Void();
}
/**
* Completable object of {@link java.lang.Boolean}.
*/
public static final class Boolean extends Values<java.lang.Boolean> { }
/**
* Completable object of {@link java.lang.CharSequence}.
*/
public static final class CharSequence extends Values<java.lang.CharSequence> { }
/**
* Completable object of {@link android.view.inputmethod.ExtractedText}.
*/
public static final class ExtractedText
extends Values<android.view.inputmethod.ExtractedText> { }
/**
* Completable object of {@link android.view.inputmethod.SurroundingText}.
*/
public static final class SurroundingText
extends Values<android.view.inputmethod.SurroundingText> { }
/**
* Completable object of {@link IInputContentUriToken>}.
*/
public static final class IInputContentUriToken
extends Values<com.android.internal.inputmethod.IInputContentUriToken> { }
/**
* Await the result by the {@link Completable.Values}.
*
* @return the result once {@link ValueBase#onComplete()}.
*/
@AnyThread
@Nullable
public static <T> T getResult(@NonNull Completable.Values<T> value) {
value.await();
return value.getValue();
}
/**
* Await the int result by the {@link Completable.Int}.
*
* @return the result once {@link ValueBase#onComplete()}.
*/
@AnyThread
public static int getIntResult(@NonNull Completable.Int value) {
value.await();
return value.getValue();
}
/**
* Await the result by the {@link Completable.Void}.
*
* Check the result once {@link ValueBase#onComplete()}
*/
@AnyThread
public static void getResult(@NonNull Completable.Void value) {
value.await();
value.getValue();
}
/**
* Await the result by the {@link Completable.Boolean}, and log it if there is no result after
* given timeout.
*
* @return the result once {@link ValueBase#onComplete()}
*/
@AnyThread
public static boolean getResultOrFalse(@NonNull Completable.Boolean value, String tag,
@NonNull String methodName, @Nullable CancellationGroup cancellationGroup,
int maxWaitTime) {
final boolean timedOut = value.await(maxWaitTime, TimeUnit.MILLISECONDS, cancellationGroup);
if (value.hasValue()) {
return value.getValue();
}
logInternal(tag, methodName, timedOut, maxWaitTime, 0);
return false;
}
/**
* Await the result by the {@link Completable.Int}, and log it if there is no result after
* given timeout.
*
* @return the result once {@link ValueBase#onComplete()}
*/
@AnyThread
public static int getResultOrZero(@NonNull Completable.Int value, String tag,
@NonNull String methodName, @Nullable CancellationGroup cancellationGroup,
int maxWaitTime) {
final boolean timedOut = value.await(maxWaitTime, TimeUnit.MILLISECONDS, cancellationGroup);
if (value.hasValue()) {
return value.getValue();
}
logInternal(tag, methodName, timedOut, maxWaitTime, 0);
return 0;
}
/**
* Await the result by the {@link Completable.Values}, and log it if there is no result after
* given timeout.
*
* @return the result once {@link ValueBase#onComplete()}
*/
@AnyThread
@Nullable
public static <T> T getResultOrNull(@NonNull Completable.Values<T> value, String tag,
@NonNull String methodName, @Nullable CancellationGroup cancellationGroup,
int maxWaitTime) {
final boolean timedOut = value.await(maxWaitTime, TimeUnit.MILLISECONDS, cancellationGroup);
if (value.hasValue()) {
return value.getValue();
}
logInternal(tag, methodName, timedOut, maxWaitTime, null);
return null;
}
@AnyThread
private static void logInternal(String tag, @Nullable String methodName, boolean timedOut,
int maxWaitTime, @Nullable Object defaultValue) {
if (timedOut) {
Log.w(tag, methodName + " didn't respond in " + maxWaitTime + " msec."
+ " Returning default: " + defaultValue);
} else {
Log.w(tag, methodName + " was canceled before complete. Returning default: "
+ defaultValue);
}
}
}

View File

@@ -0,0 +1,253 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.inputmethod;
import android.annotation.AnyThread;
import android.annotation.DurationMillisLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Log;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* A set of helper methods to retrieve result values from {@link CompletableFuture}.
*/
public final class CompletableFutureUtil {
/**
* Not intended to be instantiated.
*/
private CompletableFutureUtil() {
}
@AnyThread
@Nullable
private static <T> T getValueOrRethrowErrorInternal(@NonNull CompletableFuture<T> future) {
boolean interrupted = false;
try {
while (true) {
try {
return future.get();
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
throw new RuntimeException(cause.getMessage(), cause.getCause());
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
@AnyThread
@Nullable
private static <T> T getValueOrNullInternal(@NonNull CompletableFuture<T> future,
@Nullable String tag, @Nullable String methodName,
@DurationMillisLong long timeoutMillis, @Nullable CancellationGroup cancellationGroup) {
// We intentionally do not use CompletableFuture.anyOf() to avoid additional object
// allocations.
final boolean needsToUnregister = cancellationGroup != null
&& cancellationGroup.tryRegisterFutureOrCancelImmediately(future);
boolean interrupted = false;
try {
while (true) {
try {
return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (CompletionException e) {
if (e.getCause() instanceof CancellationException) {
logCancellationInternal(tag, methodName);
return null;
}
logErrorInternal(tag, methodName, e.getMessage());
return null;
} catch (CancellationException e) {
logCancellationInternal(tag, methodName);
return null;
} catch (InterruptedException e) {
interrupted = true;
} catch (TimeoutException e) {
logTimeoutInternal(tag, methodName, timeoutMillis);
return null;
} catch (Throwable e) {
logErrorInternal(tag, methodName, e.getMessage());
return null;
}
}
} finally {
if (needsToUnregister) {
cancellationGroup.unregisterFuture(future);
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
@AnyThread
private static void logTimeoutInternal(@Nullable String tag, @Nullable String methodName,
@DurationMillisLong long timeout) {
if (tag == null || methodName == null) {
return;
}
Log.w(tag, methodName + " didn't respond in " + timeout + " msec.");
}
@AnyThread
private static void logErrorInternal(@Nullable String tag, @Nullable String methodName,
@Nullable String errorString) {
if (tag == null || methodName == null) {
return;
}
Log.w(tag, methodName + " was failed with an exception=" + errorString);
}
@AnyThread
private static void logCancellationInternal(@Nullable String tag, @Nullable String methodName) {
if (tag == null || methodName == null) {
return;
}
Log.w(tag, methodName + " was cancelled.");
}
/**
* Return the result of the given {@link CompletableFuture<T>}.
*
* <p>This method may throw exception is the task is completed with an error.</p>
*
* @param future the object to extract the result from.
* @param <T> type of the result.
* @return the result.
*/
@AnyThread
@Nullable
public static <T> T getResult(@NonNull CompletableFuture<T> future) {
return getValueOrRethrowErrorInternal(future);
}
/**
* Return the result of the given {@link CompletableFuture<Boolean>}.
*
* <p>This method may throw exception is the task is completed with an error.</p>
*
* @param future the object to extract the result from.
* @return the result.
*/
@AnyThread
public static boolean getBooleanResult(@NonNull CompletableFuture<Boolean> future) {
return getValueOrRethrowErrorInternal(future);
}
/**
* Return the result of the given {@link CompletableFuture<Integer>}.
*
* <p>This method may throw exception is the task is completed with an error.</p>
*
* @param future the object to extract the result from.
* @return the result.
*/
@AnyThread
public static int getIntegerResult(@NonNull CompletableFuture<Integer> future) {
return getValueOrRethrowErrorInternal(future);
}
/**
* Return the result of the given {@link CompletableFuture<Boolean>}.
*
* <p>This method is agnostic to {@link Thread#interrupt()}.</p>
*
* <p>CAVEAT: when {@code cancellationGroup} is specified and it is signalled, {@code future}
* will be cancelled permanently. You have to duplicate the {@link CompletableFuture} if you
* want to avoid this side-effect.</p>
*
* @param future the object to extract the result from.
* @param tag tag name for logging. Pass {@code null} to disable logging.
* @param methodName method name for logging. Pass {@code null} to disable logging.
* @param cancellationGroup an optional {@link CancellationGroup} to cancel {@code future}
* object. Can be {@code null}.
* @param timeoutMillis length of the timeout in millisecond.
* @return the result if it is completed within the given timeout. {@code false} otherwise.
*/
@AnyThread
public static boolean getResultOrFalse(@NonNull CompletableFuture<Boolean> future,
@Nullable String tag, @Nullable String methodName,
@Nullable CancellationGroup cancellationGroup,
@DurationMillisLong long timeoutMillis) {
final Boolean obj = getValueOrNullInternal(future, tag, methodName, timeoutMillis,
cancellationGroup);
return obj != null ? obj : false;
}
/**
* Return the result of the given {@link CompletableFuture<Integer>}.
*
* <p>This method is agnostic to {@link Thread#interrupt()}.</p>
*
* <p>CAVEAT: when {@code cancellationGroup} is specified and it is signalled, {@code future}
* will be cancelled permanently. You have to duplicate the {@link CompletableFuture} if you
* want to avoid this side-effect.</p>
*
* @param future the object to extract the result from.
* @param tag tag name for logging. Pass {@code null} to disable logging.
* @param methodName method name for logging. Pass {@code null} to disable logging.
* @param cancellationGroup an optional {@link CancellationGroup} to cancel {@code future}
* object. Can be {@code null}.
* @param timeoutMillis length of the timeout in millisecond.
* @return the result if it is completed within the given timeout. {@code 0} otherwise.
*/
@AnyThread
public static int getResultOrZero(@NonNull CompletableFuture<Integer> future,
@Nullable String tag, @Nullable String methodName,
@Nullable CancellationGroup cancellationGroup, @DurationMillisLong long timeoutMillis) {
final Integer obj = getValueOrNullInternal(future, tag, methodName, timeoutMillis,
cancellationGroup);
return obj != null ? obj : 0;
}
/**
* Return the result of the given {@link CompletableFuture<T>}.
*
* <p>This method is agnostic to {@link Thread#interrupt()}.</p>
*
* <p>CAVEAT: when {@code cancellationGroup} is specified and it is signalled, {@code future}
* will be cancelled permanently. You have to duplicate the {@link CompletableFuture} if you
* want to avoid this side-effect.</p>
*
* @param future the object to extract the result from.
* @param tag tag name for logging. Pass {@code null} to disable logging.
* @param methodName method name for logging. Pass {@code null} to disable logging.
* @param cancellationGroup an optional {@link CancellationGroup} to cancel {@code future}
* object. Can be {@code null}.
* @param timeoutMillis length of the timeout in millisecond.
* @param <T> Type of the result.
* @return the result if it is completed within the given timeout. {@code null} otherwise.
*/
@AnyThread
@Nullable
public static <T> T getResultOrNull(@NonNull CompletableFuture<T> future, @Nullable String tag,
@Nullable String methodName, @Nullable CancellationGroup cancellationGroup,
@DurationMillisLong long timeoutMillis) {
return getValueOrNullInternal(future, tag, methodName, timeoutMillis, cancellationGroup);
}
}

View File

@@ -23,16 +23,19 @@ import android.os.RemoteException;
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.SurroundingText;
import com.android.internal.view.IInputContext;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* A stateless wrapper of {@link com.android.internal.view.IInputContext} to encapsulate boilerplate
* code around {@link Completable} and {@link RemoteException}.
* code around {@link CompletableFuture} and {@link RemoteException}.
*/
public final class IInputContextInvoker {
@@ -60,17 +63,17 @@ public final class IInputContextInvoker {
*
* @param length {@code length} parameter to be passed.
* @param flags {@code flags} parameter to be passed.
* @return {@link Completable.CharSequence} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<CharSequence>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.CharSequence getTextAfterCursor(int length, int flags) {
final Completable.CharSequence value = Completable.createCharSequence();
public CompletableFuture<CharSequence> getTextAfterCursor(int length, int flags) {
final CompletableFuture<CharSequence> value = new CompletableFuture<>();
try {
mIInputContext.getTextAfterCursor(length, flags, ResultCallbacks.of(value));
mIInputContext.getTextAfterCursor(length, flags, ResultCallbacks.ofCharSequence(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -80,17 +83,18 @@ public final class IInputContextInvoker {
*
* @param length {@code length} parameter to be passed.
* @param flags {@code flags} parameter to be passed.
* @return {@link Completable.CharSequence} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<CharSequence>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.CharSequence getTextBeforeCursor(int length, int flags) {
final Completable.CharSequence value = Completable.createCharSequence();
public CompletableFuture<CharSequence> getTextBeforeCursor(int length, int flags) {
final CompletableFuture<CharSequence> value = new CompletableFuture<>();
try {
mIInputContext.getTextBeforeCursor(length, flags, ResultCallbacks.of(value));
mIInputContext.getTextBeforeCursor(length, flags,
ResultCallbacks.ofCharSequence(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -99,17 +103,17 @@ public final class IInputContextInvoker {
* Invokes {@link IInputContext#getSelectedText(int, ICharSequenceResultCallback)}.
*
* @param flags {@code flags} parameter to be passed.
* @return {@link Completable.CharSequence} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<CharSequence>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.CharSequence getSelectedText(int flags) {
final Completable.CharSequence value = Completable.createCharSequence();
public CompletableFuture<CharSequence> getSelectedText(int flags) {
final CompletableFuture<CharSequence> value = new CompletableFuture<>();
try {
mIInputContext.getSelectedText(flags, ResultCallbacks.of(value));
mIInputContext.getSelectedText(flags, ResultCallbacks.ofCharSequence(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -121,19 +125,19 @@ public final class IInputContextInvoker {
* @param beforeLength {@code beforeLength} parameter to be passed.
* @param afterLength {@code afterLength} parameter to be passed.
* @param flags {@code flags} parameter to be passed.
* @return {@link Completable.SurroundingText} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<SurroundingText>} that can be used to retrieve the
* invocation result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.SurroundingText getSurroundingText(int beforeLength, int afterLength,
public CompletableFuture<SurroundingText> getSurroundingText(int beforeLength, int afterLength,
int flags) {
final Completable.SurroundingText value = Completable.createSurroundingText();
final CompletableFuture<SurroundingText> value = new CompletableFuture<>();
try {
mIInputContext.getSurroundingText(beforeLength, afterLength, flags,
ResultCallbacks.of(value));
ResultCallbacks.ofSurroundingText(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -142,17 +146,17 @@ public final class IInputContextInvoker {
* Invokes {@link IInputContext#getCursorCapsMode(int, IIntResultCallback)}.
*
* @param reqModes {@code reqModes} parameter to be passed.
* @return {@link Completable.Int} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<Integer>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.Int getCursorCapsMode(int reqModes) {
final Completable.Int value = Completable.createInt();
public CompletableFuture<Integer> getCursorCapsMode(int reqModes) {
final CompletableFuture<Integer> value = new CompletableFuture<>();
try {
mIInputContext.getCursorCapsMode(reqModes, ResultCallbacks.of(value));
mIInputContext.getCursorCapsMode(reqModes, ResultCallbacks.ofInteger(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -163,17 +167,18 @@ public final class IInputContextInvoker {
*
* @param request {@code request} parameter to be passed.
* @param flags {@code flags} parameter to be passed.
* @return {@link Completable.ExtractedText} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<ExtractedText>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
final Completable.ExtractedText value = Completable.createExtractedText();
public CompletableFuture<ExtractedText> getExtractedText(ExtractedTextRequest request,
int flags) {
final CompletableFuture<ExtractedText> value = new CompletableFuture<>();
try {
mIInputContext.getExtractedText(request, flags, ResultCallbacks.of(value));
mIInputContext.getExtractedText(request, flags, ResultCallbacks.ofExtractedText(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -474,17 +479,17 @@ public final class IInputContextInvoker {
* Invokes {@link IInputContext#requestCursorUpdates(int, IIntResultCallback)}.
*
* @param cursorUpdateMode {@code cursorUpdateMode} parameter to be passed.
* @return {@link Completable.Boolean} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<Boolean>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.Boolean requestCursorUpdates(int cursorUpdateMode) {
final Completable.Boolean value = Completable.createBoolean();
public CompletableFuture<Boolean> requestCursorUpdates(int cursorUpdateMode) {
final CompletableFuture<Boolean> value = new CompletableFuture<>();
try {
mIInputContext.requestCursorUpdates(cursorUpdateMode, ResultCallbacks.of(value));
mIInputContext.requestCursorUpdates(cursorUpdateMode, ResultCallbacks.ofBoolean(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}
@@ -496,18 +501,19 @@ public final class IInputContextInvoker {
* @param inputContentInfo {@code inputContentInfo} parameter to be passed.
* @param flags {@code flags} parameter to be passed.
* @param opts {@code opts} parameter to be passed.
* @return {@link Completable.Boolean} that can be used to retrieve the invocation result.
* {@link RemoteException} will be treated as an error.
* @return {@link CompletableFuture<Boolean>} that can be used to retrieve the invocation
* result. {@link RemoteException} will be treated as an error.
*/
@AnyThread
@NonNull
public Completable.Boolean commitContent(InputContentInfo inputContentInfo, int flags,
public CompletableFuture<Boolean> commitContent(InputContentInfo inputContentInfo, int flags,
Bundle opts) {
final Completable.Boolean value = Completable.createBoolean();
final CompletableFuture<Boolean> value = new CompletableFuture<>();
try {
mIInputContext.commitContent(inputContentInfo, flags, opts, ResultCallbacks.of(value));
mIInputContext.commitContent(inputContentInfo, flags, opts,
ResultCallbacks.ofBoolean(value));
} catch (RemoteException e) {
value.onError(ThrowableHolder.of(e));
value.completeExceptionally(e);
}
return value;
}

View File

@@ -30,6 +30,7 @@ import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* A utility class to take care of boilerplate code around IPCs.
@@ -156,10 +157,10 @@ public final class InputMethodPrivilegedOperations {
return null;
}
try {
final Completable.IInputContentUriToken value =
Completable.createIInputContentUriToken();
ops.createInputContentUriToken(contentUri, packageName, ResultCallbacks.of(value));
return Completable.getResult(value);
final CompletableFuture<IInputContentUriToken> value = new CompletableFuture<>();
ops.createInputContentUriToken(contentUri, packageName,
ResultCallbacks.ofIInputContentUriToken(value));
return CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
// For historical reasons, this error was silently ignored.
// Note that the caller already logs error so we do not need additional Log.e() here.
@@ -218,9 +219,9 @@ public final class InputMethodPrivilegedOperations {
return;
}
try {
final Completable.Void value = Completable.createVoid();
ops.setInputMethod(id, ResultCallbacks.of(value));
Completable.getResult(value);
final CompletableFuture<Void> value = new CompletableFuture<>();
ops.setInputMethod(id, ResultCallbacks.ofVoid(value));
CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -241,9 +242,9 @@ public final class InputMethodPrivilegedOperations {
return;
}
try {
final Completable.Void value = Completable.createVoid();
ops.setInputMethodAndSubtype(id, subtype, ResultCallbacks.of(value));
Completable.getResult(value);
final CompletableFuture<Void> value = new CompletableFuture<>();
ops.setInputMethodAndSubtype(id, subtype, ResultCallbacks.ofVoid(value));
CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -263,9 +264,9 @@ public final class InputMethodPrivilegedOperations {
return;
}
try {
final Completable.Void value = Completable.createVoid();
ops.hideMySoftInput(flags, ResultCallbacks.of(value));
Completable.getResult(value);
final CompletableFuture<Void> value = new CompletableFuture<>();
ops.hideMySoftInput(flags, ResultCallbacks.ofVoid(value));
CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -285,9 +286,9 @@ public final class InputMethodPrivilegedOperations {
return;
}
try {
final Completable.Void value = Completable.createVoid();
ops.showMySoftInput(flags, ResultCallbacks.of(value));
Completable.getResult(value);
final CompletableFuture<Void> value = new CompletableFuture<>();
ops.showMySoftInput(flags, ResultCallbacks.ofVoid(value));
CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -306,9 +307,9 @@ public final class InputMethodPrivilegedOperations {
return false;
}
try {
final Completable.Boolean value = Completable.createBoolean();
ops.switchToPreviousInputMethod(ResultCallbacks.of(value));
return Completable.getResult(value);
final CompletableFuture<Boolean> value = new CompletableFuture<>();
ops.switchToPreviousInputMethod(ResultCallbacks.ofBoolean(value));
return CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -329,9 +330,9 @@ public final class InputMethodPrivilegedOperations {
return false;
}
try {
final Completable.Boolean value = Completable.createBoolean();
ops.switchToNextInputMethod(onlyCurrentIme, ResultCallbacks.of(value));
return Completable.getResult(value);
final CompletableFuture<Boolean> value = new CompletableFuture<>();
ops.switchToNextInputMethod(onlyCurrentIme, ResultCallbacks.ofBoolean(value));
return CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -350,9 +351,9 @@ public final class InputMethodPrivilegedOperations {
return false;
}
try {
final Completable.Boolean value = Completable.createBoolean();
ops.shouldOfferSwitchingToNextInputMethod(ResultCallbacks.of(value));
return Completable.getResult(value);
final CompletableFuture<Boolean> value = new CompletableFuture<>();
ops.shouldOfferSwitchingToNextInputMethod(ResultCallbacks.ofBoolean(value));
return CompletableFutureUtil.getResult(value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}

View File

@@ -20,12 +20,15 @@ import android.annotation.AnyThread;
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.SurroundingText;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
/**
* Defines a set of factory methods to create {@link android.os.IBinder}-based callbacks that are
* associated with completable objects defined in {@link Completable}.
* associated with completable objects defined in {@link CompletableFuture}.
*/
public final class ResultCallbacks {
@@ -35,6 +38,13 @@ public final class ResultCallbacks {
private ResultCallbacks() {
}
private static final class LightweightThrowable extends RuntimeException {
LightweightThrowable(@Nullable ThrowableHolder throwableHolder) {
super(throwableHolder != null ? throwableHolder.getMessage() : null,
null, false, false);
}
}
@AnyThread
@Nullable
private static <T> T unwrap(@NonNull AtomicReference<T> atomicRef) {
@@ -43,218 +53,222 @@ public final class ResultCallbacks {
}
/**
* Creates {@link IIntResultCallback.Stub} that is to set {@link Completable.Int} when receiving
* the result.
* Creates {@link IIntResultCallback.Stub} that is to set {@link CompletableFuture<Integer>}
* when receiving the result.
*
* @param value {@link Completable.Int} to be set when receiving the result.
* @param value {@link CompletableFuture<Integer>} to be set when receiving the result.
* @return {@link IIntResultCallback.Stub} that can be passed as a binder IPC parameter.
*/
@AnyThread
public static IIntResultCallback.Stub of(@NonNull Completable.Int value) {
final AtomicReference<Completable.Int> atomicRef = new AtomicReference<>(value);
public static IIntResultCallback.Stub ofInteger(@NonNull CompletableFuture<Integer> value) {
final AtomicReference<CompletableFuture<Integer>> atomicRef = new AtomicReference<>(value);
return new IIntResultCallback.Stub() {
@BinderThread
@Override
public void onResult(int result) {
final Completable.Int value = unwrap(atomicRef);
final CompletableFuture<Integer> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
value.complete(result);
}
@BinderThread
@Override
public void onError(ThrowableHolder throwableHolder) {
final Completable.Int value = unwrap(atomicRef);
final CompletableFuture<Integer> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onError(throwableHolder);
value.completeExceptionally(new LightweightThrowable(throwableHolder));
}
};
}
/**
* Creates {@link ICharSequenceResultCallback.Stub} that is to set
* {@link Completable.CharSequence} when receiving the result.
* {@link CompletableFuture<CharSequence>} when receiving the result.
*
* @param value {@link Completable.CharSequence} to be set when receiving the result.
* @param value {@link CompletableFuture<CharSequence>} to be set when receiving the result.
* @return {@link ICharSequenceResultCallback.Stub} that can be passed as a binder IPC
* parameter.
*/
@AnyThread
public static ICharSequenceResultCallback.Stub of(
@NonNull Completable.CharSequence value) {
final AtomicReference<Completable.CharSequence> atomicRef = new AtomicReference<>(value);
public static ICharSequenceResultCallback.Stub ofCharSequence(
@NonNull CompletableFuture<CharSequence> value) {
final AtomicReference<CompletableFuture<CharSequence>> atomicRef =
new AtomicReference<>(value);
return new ICharSequenceResultCallback.Stub() {
@BinderThread
@Override
public void onResult(CharSequence result) {
final Completable.CharSequence value = unwrap(atomicRef);
final CompletableFuture<CharSequence> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
value.complete(result);
}
};
}
/**
* Creates {@link IExtractedTextResultCallback.Stub} that is to set
* {@link Completable.ExtractedText} when receiving the result.
* {@link CompletableFuture<ExtractedText>} when receiving the result.
*
* @param value {@link Completable.ExtractedText} to be set when receiving the result.
* @param value {@link CompletableFuture<ExtractedText>} to be set when receiving the result.
* @return {@link IExtractedTextResultCallback.Stub} that can be passed as a binder IPC
* parameter.
*/
@AnyThread
public static IExtractedTextResultCallback.Stub of(
@NonNull Completable.ExtractedText value) {
final AtomicReference<Completable.ExtractedText> atomicRef = new AtomicReference<>(value);
public static IExtractedTextResultCallback.Stub ofExtractedText(
@NonNull CompletableFuture<ExtractedText> value) {
final AtomicReference<CompletableFuture<ExtractedText>> atomicRef =
new AtomicReference<>(value);
return new IExtractedTextResultCallback.Stub() {
@BinderThread
@Override
public void onResult(android.view.inputmethod.ExtractedText result) {
final Completable.ExtractedText value = unwrap(atomicRef);
final CompletableFuture<ExtractedText> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
value.complete(result);
}
};
}
/**
* Creates {@link ISurroundingTextResultCallback.Stub} that is to set
* {@link Completable.SurroundingText} when receiving the result.
* {@link CompletableFuture<SurroundingText>} when receiving the result.
*
* @param value {@link Completable.SurroundingText} to be set when receiving the result.
* @param value {@link CompletableFuture<SurroundingText>} to be set when receiving the result.
* @return {@link ISurroundingTextResultCallback.Stub} that can be passed as a binder IPC
* parameter.
*/
@AnyThread
public static ISurroundingTextResultCallback.Stub of(
@NonNull Completable.SurroundingText value) {
final AtomicReference<Completable.SurroundingText> atomicRef = new AtomicReference<>(value);
public static ISurroundingTextResultCallback.Stub ofSurroundingText(
@NonNull CompletableFuture<SurroundingText> value) {
final AtomicReference<CompletableFuture<SurroundingText>> atomicRef =
new AtomicReference<>(value);
return new ISurroundingTextResultCallback.Stub() {
@BinderThread
@Override
public void onResult(android.view.inputmethod.SurroundingText result) {
final Completable.SurroundingText value = unwrap(atomicRef);
final CompletableFuture<SurroundingText> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
value.complete(result);
}
};
}
/**
* Creates {@link IBooleanResultCallback.Stub} that is to set {@link Completable.Boolean} when
* receiving the result.
* Creates {@link IBooleanResultCallback.Stub} that is to set {@link CompletableFuture<Boolean>}
* when receiving the result.
*
* @param value {@link Completable.Boolean} to be set when receiving the result.
* @param value {@link CompletableFuture<Boolean>} to be set when receiving the result.
* @return {@link IBooleanResultCallback.Stub} that can be passed as a binder IPC parameter.
*/
@AnyThread
public static IBooleanResultCallback.Stub of(@NonNull Completable.Boolean value) {
final AtomicReference<Completable.Boolean> atomicRef = new AtomicReference<>(value);
public static IBooleanResultCallback.Stub ofBoolean(@NonNull CompletableFuture<Boolean> value) {
final AtomicReference<CompletableFuture<Boolean>> atomicRef = new AtomicReference<>(value);
return new IBooleanResultCallback.Stub() {
@BinderThread
@Override
public void onResult(boolean result) {
final Completable.Boolean value = unwrap(atomicRef);
final CompletableFuture<Boolean> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
value.complete(result);
}
@BinderThread
@Override
public void onError(ThrowableHolder throwableHolder) {
final Completable.Boolean value = unwrap(atomicRef);
final CompletableFuture<Boolean> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onError(throwableHolder);
value.completeExceptionally(new LightweightThrowable(throwableHolder));
}
};
}
/**
* Creates {@link IVoidResultCallback.Stub} that is to set {@link Completable.Void} when
* Creates {@link IVoidResultCallback.Stub} that is to set {@link CompletableFuture<Void>} when
* receiving the result.
*
* @param value {@link Completable.Void} to be set when receiving the result.
* @param value {@link CompletableFuture<Void>} to be set when receiving the result.
* @return {@link IVoidResultCallback.Stub} that can be passed as a binder IPC parameter.
*/
@AnyThread
public static IVoidResultCallback.Stub of(@NonNull Completable.Void value) {
final AtomicReference<Completable.Void> atomicRef = new AtomicReference<>(value);
public static IVoidResultCallback.Stub ofVoid(@NonNull CompletableFuture<Void> value) {
final AtomicReference<CompletableFuture<Void>> atomicRef = new AtomicReference<>(value);
return new IVoidResultCallback.Stub() {
@BinderThread
@Override
public void onResult() {
final Completable.Void value = unwrap(atomicRef);
final CompletableFuture<Void> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete();
value.complete(null);
}
@BinderThread
@Override
public void onError(ThrowableHolder throwableHolder) {
final Completable.Void value = unwrap(atomicRef);
final CompletableFuture<Void> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onError(throwableHolder);
value.completeExceptionally(new LightweightThrowable(throwableHolder));
}
};
}
/**
* Creates {@link IInputContentUriTokenResultCallback.Stub} that is to set
* {@link Completable.IInputContentUriToken} when receiving the result.
* {@link CompletableFuture<IInputContentUriToken>} when receiving the result.
*
* @param value {@link Completable.IInputContentUriToken} to be set when receiving the result.
* @param value {@link CompletableFuture<IInputContentUriToken>} to be set when receiving the
* result.
* @return {@link IInputContentUriTokenResultCallback.Stub} that can be passed as a binder IPC
* parameter.
*/
@AnyThread
public static IInputContentUriTokenResultCallback.Stub of(
@NonNull Completable.IInputContentUriToken value) {
final AtomicReference<Completable.IInputContentUriToken>
public static IInputContentUriTokenResultCallback.Stub ofIInputContentUriToken(
@NonNull CompletableFuture<IInputContentUriToken> value) {
final AtomicReference<CompletableFuture<IInputContentUriToken>>
atomicRef = new AtomicReference<>(value);
return new IInputContentUriTokenResultCallback.Stub() {
@BinderThread
@Override
public void onResult(IInputContentUriToken result) {
final Completable.IInputContentUriToken value = unwrap(atomicRef);
final CompletableFuture<IInputContentUriToken> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
value.complete(result);
}
@BinderThread
@Override
public void onError(ThrowableHolder throwableHolder) {
final Completable.IInputContentUriToken value = unwrap(atomicRef);
final CompletableFuture<IInputContentUriToken> value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onError(throwableHolder);
value.completeExceptionally(new LightweightThrowable(throwableHolder));
}
};
}

View File

@@ -0,0 +1,284 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.inputmethod
import android.annotation.DurationMillisLong
import android.os.Handler
import android.os.SystemClock
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
@DurationMillisLong
const val SHORT_PERIOD_MILLI = 50L
const val SHORT_PERIOD_NANO = SHORT_PERIOD_MILLI * 1_000_000L
@DurationMillisLong
const val TIMEOUT_MILLI = 10_000L
const val TIMEOUT_NANO = TIMEOUT_MILLI * 1_000_000L
const val ERROR_MESSAGE = "Test Error Message!"
@LargeTest
@RunWith(AndroidJUnit4::class)
class CompletableFutureUtilTest {
private inline fun assertRuntimeException(expectedMessage: String, block: () -> Unit) {
try {
block()
fail()
} catch (exception: RuntimeException) {
assertThat(exception.message).isEqualTo(expectedMessage)
// Expected
} catch (exception: Throwable) {
fail("RuntimeException is expected but got $exception")
}
}
private inline fun runOnMainDelayed(delay: Long, crossinline block: () -> Unit) {
val handler = Handler.createAsync(
InstrumentationRegistry.getInstrumentation().getTargetContext().getMainLooper())
handler.postDelayed({
block()
}, delay)
}
@Test
fun testCharSequenceTimedOut() {
val completable = CompletableFuture<CharSequence>()
assertThat(completable.isDone).isFalse()
val beginNanos = SystemClock.elapsedRealtimeNanos()
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, null, SHORT_PERIOD_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(completable.isDone).isFalse()
assertThat(result).isNull()
assertThat(elapsed).isGreaterThan(SHORT_PERIOD_NANO)
}
@Test
fun testCharSequenceTimedOutWithInterruption() {
val completable = CompletableFuture<CharSequence>()
val beginNanosRef = AtomicLong()
val endNanosRef = AtomicLong()
val isInterruptedRef = AtomicBoolean()
val resultRef = AtomicReference<CharSequence>()
// Verifies that calling getResultOrNull() on an interrupted thread still times out with
// preserving the interrupted state.
val thread = Thread {
val currentThread = Thread.currentThread()
currentThread.interrupt()
beginNanosRef.set(SystemClock.elapsedRealtimeNanos())
resultRef.set(CompletableFutureUtil.getResultOrNull(
completable, null, null, null, SHORT_PERIOD_MILLI))
endNanosRef.set(SystemClock.elapsedRealtimeNanos())
isInterruptedRef.set(currentThread.isInterrupted())
}
thread.run()
thread.join(TIMEOUT_MILLI)
assertThat(thread.isAlive).isFalse()
val elapsedTime = endNanosRef.get() - beginNanosRef.get()
assertThat(elapsedTime).isGreaterThan(SHORT_PERIOD_NANO)
assertThat(resultRef.get()).isNull()
assertThat(isInterruptedRef.get()).isTrue()
}
@Test
fun testCharSequenceAfterCompletion() {
val expectedValue = "Expected Value"
val completable = CompletableFuture<CharSequence>()
assertThat(completable.isDone).isFalse()
completable.complete(expectedValue)
assertThat(completable.isDone).isTrue()
val beginNanos = SystemClock.elapsedRealtimeNanos()
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, null,
TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(result).isEqualTo(expectedValue)
assertThat(elapsed).isLessThan(SHORT_PERIOD_NANO)
}
@Test
fun testCharSequenceAfterError() {
val completable = CompletableFuture<CharSequence>()
assertThat(completable.isDone).isFalse()
completable.completeExceptionally(UnsupportedOperationException(ERROR_MESSAGE))
assertThat(completable.isDone).isTrue()
val beginNanos = SystemClock.elapsedRealtimeNanos()
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, null, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(result).isNull()
assertThat(elapsed).isLessThan(SHORT_PERIOD_NANO)
assertRuntimeException(ERROR_MESSAGE) {
CompletableFutureUtil.getResult(completable)
}
}
@Test
fun testCharSequenceAfterCancellation() {
val completable = CompletableFuture<CharSequence>()
val cancellationGroup = CancellationGroup()
cancellationGroup.cancelAll()
val beginNanos = SystemClock.elapsedRealtimeNanos()
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, cancellationGroup, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
// due to the side-effect of cancellationGroup, the object is already completed here.
assertThat(completable.isDone).isTrue()
assertThat(result).isNull()
assertThat(elapsed).isLessThan(SHORT_PERIOD_NANO)
// as the object is already cancelled due to the side-effect of cancellationGroup, it cannot
// accept a result any more.
completable.complete("Hello!")
assertThat(completable.isCancelled).isTrue()
}
@Test
fun testCharSequenceAfterCompleteAndCancellation() {
val expectedValue = "Expected Value"
val completable = CompletableFuture<CharSequence>()
completable.complete(expectedValue)
val cancellationGroup = CancellationGroup()
cancellationGroup.cancelAll()
val beginNanos = SystemClock.elapsedRealtimeNanos()
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, cancellationGroup, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(result).isEqualTo(expectedValue)
assertThat(CompletableFutureUtil.getResult(completable)).isEqualTo(expectedValue)
assertThat(elapsed).isLessThan(SHORT_PERIOD_NANO)
}
@Test
fun testCharSequenceMultipleAssignment() {
val expectedValue = "Expected Value"
val notExpectedValue = "Not Expected Value"
val completable = CompletableFuture<CharSequence>()
completable.complete(expectedValue)
completable.complete(notExpectedValue)
assertThat(completable.isDone).isTrue()
assertThat(CompletableFutureUtil.getResult(completable)).isEqualTo(expectedValue)
}
@Test
fun testCharSequenceUnblockByCompletion() {
val expectedValue = "Expected Value"
val completable = CompletableFuture<CharSequence>()
val beginNanos = SystemClock.elapsedRealtimeNanos()
runOnMainDelayed(SHORT_PERIOD_MILLI) {
completable.complete(expectedValue)
}
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, null, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(completable.isDone).isTrue()
assertThat(result).isEqualTo(expectedValue)
assertThat(elapsed).isIn(Range.closedOpen(SHORT_PERIOD_NANO, TIMEOUT_NANO))
}
@Test
fun testCharSequenceUnblockByCompletionWithCancellationGroup() {
val expectedValue = "Expected Value"
val completable = CompletableFuture<CharSequence>()
var cancellationGroup = CancellationGroup()
assertThat(cancellationGroup.isCanceled).isFalse()
val beginNanos = SystemClock.elapsedRealtimeNanos()
runOnMainDelayed(SHORT_PERIOD_MILLI) {
completable.complete(expectedValue)
}
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, cancellationGroup, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(cancellationGroup.isCanceled).isFalse()
assertThat(completable.isDone).isTrue()
assertThat(result).isEqualTo(expectedValue)
assertThat(elapsed).isIn(Range.closedOpen(SHORT_PERIOD_NANO, TIMEOUT_NANO))
}
@Test
fun testCharSequenceUnblockByError() {
val completable = CompletableFuture<CharSequence>()
val beginNanos = SystemClock.elapsedRealtimeNanos()
runOnMainDelayed(SHORT_PERIOD_MILLI) {
completable.completeExceptionally(UnsupportedOperationException(ERROR_MESSAGE))
}
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, null, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
assertThat(completable.isDone).isTrue()
assertThat(result).isNull()
assertThat(elapsed).isIn(Range.closedOpen(SHORT_PERIOD_NANO, TIMEOUT_NANO))
}
@Test
fun testCharSequenceUnblockByCancellation() {
val completable = CompletableFuture<CharSequence>()
val cancellationGroup = CancellationGroup()
val beginNanos = SystemClock.elapsedRealtimeNanos()
runOnMainDelayed(SHORT_PERIOD_MILLI) {
cancellationGroup.cancelAll()
}
val result = CompletableFutureUtil.getResultOrNull(
completable, null, null, cancellationGroup, TIMEOUT_MILLI)
val elapsed = SystemClock.elapsedRealtimeNanos() - beginNanos
// due to the side-effect of cancellationGroup.
assertThat(completable.isDone).isTrue()
assertThat(result).isNull()
assertThat(elapsed).isIn(Range.closedOpen(SHORT_PERIOD_NANO, TIMEOUT_NANO))
}
}