/*
 * 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;
        }
    }
}
