| /* |
| * 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 android.inputmethodservice; |
| |
| import static android.view.inputmethod.TextBoundsInfoResult.CODE_CANCELLED; |
| |
| import android.annotation.AnyThread; |
| import android.annotation.CallbackExecutor; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.graphics.RectF; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.CancellationSignalBeamer; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| 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.HandwritingGesture; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.InputContentInfo; |
| import android.view.inputmethod.ParcelableHandwritingGesture; |
| import android.view.inputmethod.SurroundingText; |
| import android.view.inputmethod.TextAttribute; |
| import android.view.inputmethod.TextBoundsInfo; |
| import android.view.inputmethod.TextBoundsInfoResult; |
| |
| import com.android.internal.infra.AndroidFuture; |
| import com.android.internal.inputmethod.IRemoteInputConnection; |
| import com.android.internal.inputmethod.InputConnectionCommandHeader; |
| |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| import java.util.function.IntConsumer; |
| |
| /** |
| * A stateless wrapper of {@link com.android.internal.inputmethod.IRemoteInputConnection} to |
| * encapsulate boilerplate code around {@link AndroidFuture} and {@link RemoteException}. |
| */ |
| final class IRemoteInputConnectionInvoker { |
| |
| @NonNull |
| private final IRemoteInputConnection mConnection; |
| private final int mSessionId; |
| private CancellationSignalBeamer.Sender mBeamer; |
| |
| private IRemoteInputConnectionInvoker(@NonNull IRemoteInputConnection inputConnection, |
| int sessionId) { |
| mConnection = inputConnection; |
| mSessionId = sessionId; |
| } |
| |
| private abstract static class OnceResultReceiver<C> extends ResultReceiver { |
| @Nullable |
| private C mConsumer; |
| @Nullable |
| private Executor mExecutor; |
| |
| protected OnceResultReceiver(@NonNull Executor executor, @NonNull C consumer) { |
| super(null); |
| Objects.requireNonNull(executor); |
| Objects.requireNonNull(consumer); |
| mExecutor = executor; |
| mConsumer = consumer; |
| } |
| |
| @Override |
| protected final void onReceiveResult(int resultCode, Bundle resultData) { |
| final Executor executor; |
| final C consumer; |
| synchronized (this) { |
| executor = mExecutor; |
| consumer = mConsumer; |
| mExecutor = null; |
| mConsumer = null; |
| } |
| if (executor != null && consumer != null) { |
| dispatch(executor, consumer, resultCode, resultData); |
| } |
| } |
| |
| protected abstract void dispatch(@NonNull Executor executor, @NonNull C consumer, int code, |
| Bundle data); |
| } |
| |
| /** |
| * Subclass of {@link ResultReceiver} used by |
| * {@link #performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} for providing |
| * callback. |
| */ |
| private static final class IntResultReceiver extends OnceResultReceiver<IntConsumer> { |
| IntResultReceiver(@NonNull Executor executor, @NonNull IntConsumer consumer) { |
| super(executor, consumer); |
| } |
| |
| @Override |
| protected void dispatch(@NonNull Executor executor, @NonNull IntConsumer consumer, int code, |
| Bundle data) { |
| executor.execute(() -> consumer.accept(code)); |
| } |
| } |
| |
| /** |
| * Subclass of {@link ResultReceiver} used by |
| * {@link #requestTextBoundsInfo(RectF, Executor, Consumer)} for providing |
| * callback. |
| */ |
| private static final class TextBoundsInfoResultReceiver extends |
| OnceResultReceiver<Consumer<TextBoundsInfoResult>> { |
| TextBoundsInfoResultReceiver(@NonNull Executor executor, |
| @NonNull Consumer<TextBoundsInfoResult> consumer) { |
| super(executor, consumer); |
| } |
| |
| @Override |
| protected void dispatch(@NonNull Executor executor, |
| @NonNull Consumer<TextBoundsInfoResult> consumer, int code, Bundle data) { |
| final TextBoundsInfoResult textBoundsInfoResult = new TextBoundsInfoResult( |
| code, TextBoundsInfo.createFromBundle(data)); |
| executor.execute(() -> consumer.accept(textBoundsInfoResult)); |
| } |
| } |
| |
| /** |
| * Creates a new instance of {@link IRemoteInputConnectionInvoker} for the given |
| * {@link IRemoteInputConnection}. |
| * |
| * @param connection {@link IRemoteInputConnection} to be wrapped. |
| * @return A new instance of {@link IRemoteInputConnectionInvoker}. |
| */ |
| public static IRemoteInputConnectionInvoker create(@NonNull IRemoteInputConnection connection) { |
| Objects.requireNonNull(connection); |
| return new IRemoteInputConnectionInvoker(connection, 0); |
| } |
| |
| /** |
| * Creates a new instance of {@link IRemoteInputConnectionInvoker} with the given |
| * {@code sessionId}. |
| * |
| * @param sessionId the new session ID to be used. |
| * @return A new instance of {@link IRemoteInputConnectionInvoker}. |
| */ |
| @NonNull |
| public IRemoteInputConnectionInvoker cloneWithSessionId(int sessionId) { |
| return new IRemoteInputConnectionInvoker(mConnection, sessionId); |
| } |
| |
| /** |
| * @param connection {@code IRemoteInputConnection} to be compared with |
| * @return {@code true} if the underlying {@code IRemoteInputConnection} is the same. |
| * {@code false} if {@code inputContext} is {@code null}. |
| */ |
| @AnyThread |
| public boolean isSameConnection(@NonNull IRemoteInputConnection connection) { |
| if (connection == null) { |
| return false; |
| } |
| return mConnection.asBinder() == connection.asBinder(); |
| } |
| |
| @NonNull |
| InputConnectionCommandHeader createHeader() { |
| return new InputConnectionCommandHeader(mSessionId); |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#getTextAfterCursor(InputConnectionCommandHeader, int, |
| * int, AndroidFuture)}. |
| * |
| * @param length {@code length} parameter to be passed. |
| * @param flags {@code flags} parameter to be passed. |
| * @return {@link AndroidFuture<CharSequence>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<CharSequence> getTextAfterCursor(int length, int flags) { |
| final AndroidFuture<CharSequence> future = new AndroidFuture<>(); |
| try { |
| mConnection.getTextAfterCursor(createHeader(), length, flags, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#getTextBeforeCursor(InputConnectionCommandHeader, int, |
| * int, AndroidFuture)}. |
| * |
| * @param length {@code length} parameter to be passed. |
| * @param flags {@code flags} parameter to be passed. |
| * @return {@link AndroidFuture<CharSequence>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<CharSequence> getTextBeforeCursor(int length, int flags) { |
| final AndroidFuture<CharSequence> future = new AndroidFuture<>(); |
| try { |
| mConnection.getTextBeforeCursor(createHeader(), length, flags, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#getSelectedText(InputConnectionCommandHeader, int, |
| * AndroidFuture)}. |
| * |
| * @param flags {@code flags} parameter to be passed. |
| * @return {@link AndroidFuture<CharSequence>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<CharSequence> getSelectedText(int flags) { |
| final AndroidFuture<CharSequence> future = new AndroidFuture<>(); |
| try { |
| mConnection.getSelectedText(createHeader(), flags, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#getSurroundingText(InputConnectionCommandHeader, int, int, int, |
| * AndroidFuture)}. |
| * |
| * @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 AndroidFuture<SurroundingText>} that can be used to retrieve the |
| * invocation result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<SurroundingText> getSurroundingText(int beforeLength, int afterLength, |
| int flags) { |
| final AndroidFuture<SurroundingText> future = new AndroidFuture<>(); |
| try { |
| mConnection.getSurroundingText(createHeader(), beforeLength, afterLength, flags, |
| future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#getCursorCapsMode(InputConnectionCommandHeader, int, |
| * AndroidFuture)}. |
| * |
| * @param reqModes {@code reqModes} parameter to be passed. |
| * @return {@link AndroidFuture<Integer>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<Integer> getCursorCapsMode(int reqModes) { |
| final AndroidFuture<Integer> future = new AndroidFuture<>(); |
| try { |
| mConnection.getCursorCapsMode(createHeader(), reqModes, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#getExtractedText(InputConnectionCommandHeader, |
| * ExtractedTextRequest, int, AndroidFuture)}. |
| * |
| * @param request {@code request} parameter to be passed. |
| * @param flags {@code flags} parameter to be passed. |
| * @return {@link AndroidFuture<ExtractedText>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<ExtractedText> getExtractedText(ExtractedTextRequest request, |
| int flags) { |
| final AndroidFuture<ExtractedText> future = new AndroidFuture<>(); |
| try { |
| mConnection.getExtractedText(createHeader(), request, flags, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#commitText(InputConnectionCommandHeader, CharSequence, int)}. |
| * |
| * @param text {@code text} parameter to be passed. |
| * @param newCursorPosition {@code newCursorPosition} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean commitText(CharSequence text, int newCursorPosition) { |
| try { |
| mConnection.commitText(createHeader(), text, newCursorPosition); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#commitTextWithTextAttribute( |
| * InputConnectionCommandHeader, int, CharSequence)}. |
| * |
| * @param text {@code text} parameter to be passed. |
| * @param newCursorPosition {@code newCursorPosition} parameter to be passed. |
| * @param textAttribute The extra information about the text. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean commitText(CharSequence text, int newCursorPosition, |
| @Nullable TextAttribute textAttribute) { |
| try { |
| mConnection.commitTextWithTextAttribute( |
| createHeader(), text, newCursorPosition, textAttribute); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#commitCompletion(InputConnectionCommandHeader, |
| * CompletionInfo)}. |
| * |
| * @param text {@code text} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean commitCompletion(CompletionInfo text) { |
| try { |
| mConnection.commitCompletion(createHeader(), text); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#commitCorrection(InputConnectionCommandHeader, |
| * CorrectionInfo)}. |
| * |
| * @param correctionInfo {@code correctionInfo} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean commitCorrection(CorrectionInfo correctionInfo) { |
| try { |
| mConnection.commitCorrection(createHeader(), correctionInfo); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#setSelection(InputConnectionCommandHeader, int, int)}. |
| * |
| * @param start {@code start} parameter to be passed. |
| * @param end {@code start} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean setSelection(int start, int end) { |
| try { |
| mConnection.setSelection(createHeader(), start, end); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#performEditorAction(InputConnectionCommandHeader, int)}. |
| * |
| * @param actionCode {@code start} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean performEditorAction(int actionCode) { |
| try { |
| mConnection.performEditorAction(createHeader(), actionCode); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#performContextMenuAction(InputConnectionCommandHeader, int)}. |
| * |
| * @param id {@code id} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean performContextMenuAction(int id) { |
| try { |
| mConnection.performContextMenuAction(createHeader(), id); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#setComposingRegion(InputConnectionCommandHeader, int, int)}. |
| * |
| * @param start {@code id} parameter to be passed. |
| * @param end {@code id} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean setComposingRegion(int start, int end) { |
| try { |
| mConnection.setComposingRegion(createHeader(), start, end); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#setComposingRegionWithTextAttribute( |
| * InputConnectionCommandHeader, int, int, TextAttribute)}. |
| * |
| * @param start {@code id} parameter to be passed. |
| * @param end {@code id} parameter to be passed. |
| * @param textAttribute The extra information about the text. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean setComposingRegion(int start, int end, @Nullable TextAttribute textAttribute) { |
| try { |
| mConnection.setComposingRegionWithTextAttribute( |
| createHeader(), start, end, textAttribute); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#setComposingText(InputConnectionCommandHeader, |
| * CharSequence, int)}. |
| * |
| * @param text {@code text} parameter to be passed. |
| * @param newCursorPosition {@code newCursorPosition} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean setComposingText(CharSequence text, int newCursorPosition) { |
| try { |
| mConnection.setComposingText(createHeader(), text, newCursorPosition); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#setComposingTextWithTextAttribute( |
| * InputConnectionCommandHeader, CharSequence, int, TextAttribute)}. |
| * |
| * @param text {@code text} parameter to be passed. |
| * @param newCursorPosition {@code newCursorPosition} parameter to be passed. |
| * @param textAttribute The extra information about the text. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean setComposingText(CharSequence text, int newCursorPosition, |
| @Nullable TextAttribute textAttribute) { |
| try { |
| mConnection.setComposingTextWithTextAttribute( |
| createHeader(), text, newCursorPosition, textAttribute); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#finishComposingText(InputConnectionCommandHeader)}. |
| * |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean finishComposingText() { |
| try { |
| mConnection.finishComposingText(createHeader()); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#beginBatchEdit(InputConnectionCommandHeader)}. |
| * |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean beginBatchEdit() { |
| try { |
| mConnection.beginBatchEdit(createHeader()); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#endBatchEdit(InputConnectionCommandHeader)}. |
| * |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean endBatchEdit() { |
| try { |
| mConnection.endBatchEdit(createHeader()); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#sendKeyEvent(InputConnectionCommandHeader, KeyEvent)}. |
| * |
| * @param event {@code event} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean sendKeyEvent(KeyEvent event) { |
| try { |
| mConnection.sendKeyEvent(createHeader(), event); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#clearMetaKeyStates(InputConnectionCommandHeader, int)}. |
| * |
| * @param states {@code states} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean clearMetaKeyStates(int states) { |
| try { |
| mConnection.clearMetaKeyStates(createHeader(), states); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#deleteSurroundingText(InputConnectionCommandHeader, int, int)}. |
| * |
| * @param beforeLength {@code beforeLength} parameter to be passed. |
| * @param afterLength {@code afterLength} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean deleteSurroundingText(int beforeLength, int afterLength) { |
| try { |
| mConnection.deleteSurroundingText(createHeader(), beforeLength, afterLength); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#deleteSurroundingTextInCodePoints( |
| * InputConnectionCommandHeader, int, int)}. |
| * |
| * @param beforeLength {@code beforeLength} parameter to be passed. |
| * @param afterLength {@code afterLength} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { |
| try { |
| mConnection.deleteSurroundingTextInCodePoints(createHeader(), beforeLength, |
| afterLength); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#performSpellCheck(InputConnectionCommandHeader)}. |
| * |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean performSpellCheck() { |
| try { |
| mConnection.performSpellCheck(createHeader()); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#performPrivateCommand(InputConnectionCommandHeader, |
| * String, Bundle)}. |
| * |
| * @param action {@code action} parameter to be passed. |
| * @param data {@code data} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean performPrivateCommand(String action, Bundle data) { |
| try { |
| mConnection.performPrivateCommand(createHeader(), action, data); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#performHandwritingGesture( |
| * InputConnectionCommandHeader, ParcelableHandwritingGesture, ResultReceiver)}. |
| */ |
| @AnyThread |
| public void performHandwritingGesture(@NonNull HandwritingGesture gesture, |
| @Nullable @CallbackExecutor Executor executor, @Nullable IntConsumer consumer) { |
| ResultReceiver resultReceiver = null; |
| if (consumer != null) { |
| Objects.requireNonNull(executor); |
| resultReceiver = new IntResultReceiver(executor, consumer); |
| } |
| try { |
| try (var ignored = getCancellationSignalBeamer().beamScopeIfNeeded(gesture)) { |
| mConnection.performHandwritingGesture(createHeader(), |
| ParcelableHandwritingGesture.of(gesture), |
| resultReceiver); |
| } |
| } catch (RemoteException e) { |
| if (consumer != null && executor != null) { |
| executor.execute(() -> consumer.accept( |
| InputConnection.HANDWRITING_GESTURE_RESULT_CANCELLED)); |
| } |
| } |
| } |
| |
| /** |
| * Invokes one of {@link IRemoteInputConnection#previewHandwritingGesture( |
| * InputConnectionCommandHeader, HandwritingGesture, IBinder)} |
| */ |
| @AnyThread |
| public boolean previewHandwritingGesture( |
| @NonNull HandwritingGesture gesture, |
| @Nullable CancellationSignal cancellationSignal) { |
| try { |
| try (var csToken = beam(cancellationSignal)) { |
| mConnection.previewHandwritingGesture(createHeader(), |
| ParcelableHandwritingGesture.of(gesture), |
| csToken); |
| } |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| @Nullable |
| CancellationSignalBeamer.Sender.CloseableToken beam(CancellationSignal cs) { |
| if (cs == null) { |
| return null; |
| } |
| return getCancellationSignalBeamer().beam(cs); |
| } |
| |
| private CancellationSignalBeamer.Sender getCancellationSignalBeamer() { |
| if (mBeamer != null) { |
| return mBeamer; |
| } |
| mBeamer = new CancellationSignalBeamer.Sender() { |
| @Override |
| public void onCancel(IBinder token) { |
| try { |
| mConnection.cancelCancellationSignal(token); |
| } catch (RemoteException e) { |
| // Remote process likely died, ignore. |
| } |
| } |
| |
| @Override |
| public void onForget(IBinder token) { |
| try { |
| mConnection.forgetCancellationSignal(token); |
| } catch (RemoteException e) { |
| // Remote process likely died, ignore. |
| } |
| } |
| }; |
| |
| return mBeamer; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#requestCursorUpdates(InputConnectionCommandHeader, int, |
| * int, AndroidFuture)}. |
| * |
| * @param cursorUpdateMode {@code cursorUpdateMode} parameter to be passed. |
| * @param imeDisplayId the display ID that is associated with the IME. |
| * @return {@link AndroidFuture<Boolean>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<Boolean> requestCursorUpdates(int cursorUpdateMode, int imeDisplayId) { |
| final AndroidFuture<Boolean> future = new AndroidFuture<>(); |
| try { |
| mConnection.requestCursorUpdates(createHeader(), cursorUpdateMode, imeDisplayId, |
| future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#requestCursorUpdatesWithFilter( |
| * InputConnectionCommandHeader, int, int, int, AndroidFuture)}. |
| * |
| * @param cursorUpdateMode {@code cursorUpdateMode} parameter to be passed. |
| * @param cursorUpdateFilter {@code cursorUpdateFilter} parameter to be passed. |
| * @param imeDisplayId the display ID that is associated with the IME. |
| * @return {@link AndroidFuture<Boolean>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<Boolean> requestCursorUpdates(int cursorUpdateMode, int cursorUpdateFilter, |
| int imeDisplayId) { |
| final AndroidFuture<Boolean> future = new AndroidFuture<>(); |
| try { |
| mConnection.requestCursorUpdatesWithFilter(createHeader(), cursorUpdateMode, |
| cursorUpdateFilter, imeDisplayId, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#requestTextBoundsInfo(InputConnectionCommandHeader, |
| * RectF, ResultReceiver)} |
| * @param bounds {@code rectF} parameter to be passed. |
| * @param executor {@code Executor} parameter to be passed. |
| * @param consumer {@code Consumer} parameter to be passed. |
| */ |
| @AnyThread |
| public void requestTextBoundsInfo( |
| @NonNull RectF bounds, @NonNull @CallbackExecutor Executor executor, |
| @NonNull Consumer<TextBoundsInfoResult> consumer) { |
| Objects.requireNonNull(executor); |
| Objects.requireNonNull(consumer); |
| |
| final ResultReceiver resultReceiver = new TextBoundsInfoResultReceiver(executor, consumer); |
| try { |
| mConnection.requestTextBoundsInfo(createHeader(), bounds, resultReceiver); |
| } catch (RemoteException e) { |
| executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_CANCELLED))); |
| } |
| } |
| |
| /** |
| * Invokes {@link IRemoteInputConnection#commitContent(InputConnectionCommandHeader, |
| * InputContentInfo, int, Bundle, AndroidFuture)}. |
| * |
| * @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 AndroidFuture<Boolean>} that can be used to retrieve the invocation |
| * result. {@link RemoteException} will be treated as an error. |
| */ |
| @AnyThread |
| @NonNull |
| public AndroidFuture<Boolean> commitContent(InputContentInfo inputContentInfo, int flags, |
| Bundle opts) { |
| final AndroidFuture<Boolean> future = new AndroidFuture<>(); |
| try { |
| mConnection.commitContent(createHeader(), inputContentInfo, flags, opts, future); |
| } catch (RemoteException e) { |
| future.completeExceptionally(e); |
| } |
| return future; |
| } |
| |
| /** |
| * Invokes |
| * {@link IRemoteInputConnection#setImeConsumesInput(InputConnectionCommandHeader, boolean)}. |
| * |
| * @param imeConsumesInput {@code imeConsumesInput} parameter to be passed. |
| * @return {@code true} if the invocation is completed without {@link RemoteException}. |
| * {@code false} otherwise. |
| */ |
| @AnyThread |
| public boolean setImeConsumesInput(boolean imeConsumesInput) { |
| try { |
| mConnection.setImeConsumesInput(createHeader(), imeConsumesInput); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Replaces the specific range in the current input field with suggested text. |
| * |
| * @param start the character index where the replacement should start. |
| * @param end the character index where the replacement should end. |
| * @param newCursorPosition the new cursor position around the text. If > 0, this is relative to |
| * the end of the text - 1; if <= 0, this is relative to the start of the text. So a value |
| * of 1 will always advance you to the position after the full text being inserted. Note |
| * that this means you can't position the cursor within the text. |
| * @param text the text to replace. This may include styles. |
| * @param textAttribute The extra information about the text. This value may be null. |
| * @return {@code true} if the specific range is replaced successfully, {@code false} otherwise. |
| * @see android.view.inputmethod.InputConnection#replaceText(int, int, CharSequence, int, |
| * TextAttribute) |
| */ |
| @AnyThread |
| public boolean replaceText( |
| int start, |
| int end, |
| @NonNull CharSequence text, |
| int newCursorPosition, |
| @Nullable TextAttribute textAttribute) { |
| try { |
| mConnection.replaceText( |
| createHeader(), start, end, text, newCursorPosition, textAttribute); |
| return true; |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| } |