diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java index ec5bcf1f1199b..464b421d970d5 100644 --- a/core/java/android/inputmethodservice/RemoteInputConnection.java +++ b/core/java/android/inputmethodservice/RemoteInputConnection.java @@ -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 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 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 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 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 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 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 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 value = + mInvoker.commitContent(inputContentInfo, flags, opts); + return CompletableFutureUtil.getResultOrFalse( value, TAG, "commitContent()", mCancellationGroup, MAX_WAIT_TIME_MILLIS); } diff --git a/core/java/com/android/internal/inputmethod/CancellationGroup.java b/core/java/com/android/internal/inputmethod/CancellationGroup.java index aef9e3b9a0fc1..3b2e1cd9b556a 100644 --- a/core/java/com/android/internal/inputmethod/CancellationGroup.java +++ b/core/java/com/android/internal/inputmethod/CancellationGroup.java @@ -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. + * + *

TODO: Make this lock-free.

*/ 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. * *

This will be lazily instantiated to avoid unnecessary object allocations.

*/ @Nullable @GuardedBy("mLock") - private ArrayList mLatchList = null; + private ArrayList> 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. + * + *

If this {@link CancellationGroup} is already cancelled, then this method will immediately + * call {@link CompletableFuture#cancel(boolean)} then return {@code false}.

+ * + *

When this method returns {@code true}, call {@link #unregisterFuture(CompletableFuture)} + * to remove the unnecessary object reference.

+ * + * @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; } } } diff --git a/core/java/com/android/internal/inputmethod/Completable.java b/core/java/com/android/internal/inputmethod/Completable.java deleted file mode 100644 index 132272c68ca86..0000000000000 --- a/core/java/com/android/internal/inputmethod/Completable.java +++ /dev/null @@ -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)}. - * - *

This is not {@code null} only when {@link #mState} is - * {@link CompletionState#COMPLETED_WITH_ERROR}.

- */ - @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. - * - *

Must be called after acquiring {@link #mStateLock}.

- * - * @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. - * - *

- *

    - *
  1. This object becomes ready to return the value.
  2. - *
  3. {@link CancellationGroup#cancelAll()} gets called.
  4. - *
  5. The given timeout period has passed.
  6. - *
- *

- * - *

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}.

- * - * @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 type associated with this completable object. - */ - public static class Values 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 { } - - /** - * Completable object of {@link java.lang.CharSequence}. - */ - public static final class CharSequence extends Values { } - - /** - * Completable object of {@link android.view.inputmethod.ExtractedText}. - */ - public static final class ExtractedText - extends Values { } - - /** - * Completable object of {@link android.view.inputmethod.SurroundingText}. - */ - public static final class SurroundingText - extends Values { } - - /** - * Completable object of {@link IInputContentUriToken>}. - */ - public static final class IInputContentUriToken - extends Values { } - - /** - * Await the result by the {@link Completable.Values}. - * - * @return the result once {@link ValueBase#onComplete()}. - */ - @AnyThread - @Nullable - public static T getResult(@NonNull Completable.Values 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 getResultOrNull(@NonNull Completable.Values 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); - } - } -} diff --git a/core/java/com/android/internal/inputmethod/CompletableFutureUtil.java b/core/java/com/android/internal/inputmethod/CompletableFutureUtil.java new file mode 100644 index 0000000000000..ec10e014384a4 --- /dev/null +++ b/core/java/com/android/internal/inputmethod/CompletableFutureUtil.java @@ -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 getValueOrRethrowErrorInternal(@NonNull CompletableFuture 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 getValueOrNullInternal(@NonNull CompletableFuture 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}. + * + *

This method may throw exception is the task is completed with an error.

+ * + * @param future the object to extract the result from. + * @param type of the result. + * @return the result. + */ + @AnyThread + @Nullable + public static T getResult(@NonNull CompletableFuture future) { + return getValueOrRethrowErrorInternal(future); + } + + /** + * Return the result of the given {@link CompletableFuture}. + * + *

This method may throw exception is the task is completed with an error.

+ * + * @param future the object to extract the result from. + * @return the result. + */ + @AnyThread + public static boolean getBooleanResult(@NonNull CompletableFuture future) { + return getValueOrRethrowErrorInternal(future); + } + + /** + * Return the result of the given {@link CompletableFuture}. + * + *

This method may throw exception is the task is completed with an error.

+ * + * @param future the object to extract the result from. + * @return the result. + */ + @AnyThread + public static int getIntegerResult(@NonNull CompletableFuture future) { + return getValueOrRethrowErrorInternal(future); + } + + /** + * Return the result of the given {@link CompletableFuture}. + * + *

This method is agnostic to {@link Thread#interrupt()}.

+ * + *

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.

+ * + * @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 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}. + * + *

This method is agnostic to {@link Thread#interrupt()}.

+ * + *

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.

+ * + * @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 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}. + * + *

This method is agnostic to {@link Thread#interrupt()}.

+ * + *

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.

+ * + * @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 Type of the result. + * @return the result if it is completed within the given timeout. {@code null} otherwise. + */ + @AnyThread + @Nullable + public static T getResultOrNull(@NonNull CompletableFuture future, @Nullable String tag, + @Nullable String methodName, @Nullable CancellationGroup cancellationGroup, + @DurationMillisLong long timeoutMillis) { + return getValueOrNullInternal(future, tag, methodName, timeoutMillis, cancellationGroup); + } +} diff --git a/core/java/com/android/internal/inputmethod/IInputContextInvoker.java b/core/java/com/android/internal/inputmethod/IInputContextInvoker.java index 977f9a5a51105..783d54d077b6a 100644 --- a/core/java/com/android/internal/inputmethod/IInputContextInvoker.java +++ b/core/java/com/android/internal/inputmethod/IInputContextInvoker.java @@ -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} 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 getTextAfterCursor(int length, int flags) { + final CompletableFuture 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} 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 getTextBeforeCursor(int length, int flags) { + final CompletableFuture 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} 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 getSelectedText(int flags) { + final CompletableFuture 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} 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 getSurroundingText(int beforeLength, int afterLength, int flags) { - final Completable.SurroundingText value = Completable.createSurroundingText(); + final CompletableFuture 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} 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 getCursorCapsMode(int reqModes) { + final CompletableFuture 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} 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 getExtractedText(ExtractedTextRequest request, + int flags) { + final CompletableFuture 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} 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 requestCursorUpdates(int cursorUpdateMode) { + final CompletableFuture 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} 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 commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { - final Completable.Boolean value = Completable.createBoolean(); + final CompletableFuture 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; } diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java index 9fb0bb52e0011..c0785abdc7fb6 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java +++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java @@ -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 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 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 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 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 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 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 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 value = new CompletableFuture<>(); + ops.shouldOfferSwitchingToNextInputMethod(ResultCallbacks.ofBoolean(value)); + return CompletableFutureUtil.getResult(value); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/com/android/internal/inputmethod/ResultCallbacks.java b/core/java/com/android/internal/inputmethod/ResultCallbacks.java index 343a6e697c7c7..8aef54b3b1d38 100644 --- a/core/java/com/android/internal/inputmethod/ResultCallbacks.java +++ b/core/java/com/android/internal/inputmethod/ResultCallbacks.java @@ -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 unwrap(@NonNull AtomicReference 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} + * when receiving the result. * - * @param value {@link Completable.Int} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 atomicRef = new AtomicReference<>(value); + public static IIntResultCallback.Stub ofInteger(@NonNull CompletableFuture value) { + final AtomicReference> atomicRef = new AtomicReference<>(value); return new IIntResultCallback.Stub() { @BinderThread @Override public void onResult(int result) { - final Completable.Int value = unwrap(atomicRef); + final CompletableFuture 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 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} when receiving the result. * - * @param value {@link Completable.CharSequence} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 atomicRef = new AtomicReference<>(value); + public static ICharSequenceResultCallback.Stub ofCharSequence( + @NonNull CompletableFuture value) { + final AtomicReference> atomicRef = + new AtomicReference<>(value); return new ICharSequenceResultCallback.Stub() { @BinderThread @Override public void onResult(CharSequence result) { - final Completable.CharSequence value = unwrap(atomicRef); + final CompletableFuture 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} when receiving the result. * - * @param value {@link Completable.ExtractedText} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 atomicRef = new AtomicReference<>(value); + public static IExtractedTextResultCallback.Stub ofExtractedText( + @NonNull CompletableFuture value) { + final AtomicReference> 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 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} when receiving the result. * - * @param value {@link Completable.SurroundingText} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 atomicRef = new AtomicReference<>(value); + public static ISurroundingTextResultCallback.Stub ofSurroundingText( + @NonNull CompletableFuture value) { + final AtomicReference> 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 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} + * when receiving the result. * - * @param value {@link Completable.Boolean} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 atomicRef = new AtomicReference<>(value); + public static IBooleanResultCallback.Stub ofBoolean(@NonNull CompletableFuture value) { + final AtomicReference> atomicRef = new AtomicReference<>(value); return new IBooleanResultCallback.Stub() { @BinderThread @Override public void onResult(boolean result) { - final Completable.Boolean value = unwrap(atomicRef); + final CompletableFuture 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 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} when * receiving the result. * - * @param value {@link Completable.Void} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 atomicRef = new AtomicReference<>(value); + public static IVoidResultCallback.Stub ofVoid(@NonNull CompletableFuture value) { + final AtomicReference> atomicRef = new AtomicReference<>(value); return new IVoidResultCallback.Stub() { @BinderThread @Override public void onResult() { - final Completable.Void value = unwrap(atomicRef); + final CompletableFuture 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 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} when receiving the result. * - * @param value {@link Completable.IInputContentUriToken} to be set when receiving the result. + * @param value {@link CompletableFuture} 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 + public static IInputContentUriTokenResultCallback.Stub ofIInputContentUriToken( + @NonNull CompletableFuture value) { + final AtomicReference> atomicRef = new AtomicReference<>(value); return new IInputContentUriTokenResultCallback.Stub() { @BinderThread @Override public void onResult(IInputContentUriToken result) { - final Completable.IInputContentUriToken value = unwrap(atomicRef); + final CompletableFuture 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 value = unwrap(atomicRef); if (value == null) { return; } - value.onError(throwableHolder); + value.completeExceptionally(new LightweightThrowable(throwableHolder)); } }; } diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt b/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt new file mode 100644 index 0000000000000..8355daa94e911 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt @@ -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() + + 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() + + val beginNanosRef = AtomicLong() + val endNanosRef = AtomicLong() + val isInterruptedRef = AtomicBoolean() + val resultRef = AtomicReference() + + // 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() + + 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() + + 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() + 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() + 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() + 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() + + 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() + 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() + + 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() + 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)) + } +}