EmojiInputConnection.java

/*
 * Copyright 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 androidx.emoji2.viewsintegration;

import android.text.Editable;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import android.widget.TextView;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.emoji2.text.EmojiCompat;

/**
 * InputConnectionWrapper for EditText delete operations. Keyboard does not have knowledge about
 * emojis and therefore might send commands to delete a part of the emoji sequence which creates
 * invalid codeunits/getCodepointAt in the text.
 * <p/>
 * This class tries to correctly delete an emoji checking if there is an emoji span.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(19)
final class EmojiInputConnection extends InputConnectionWrapper {
    private final TextView mTextView;
    private final EmojiCompatDeleteHelper mEmojiCompatDeleteHelper;

    EmojiInputConnection(
            @NonNull final TextView textView,
            @NonNull final InputConnection inputConnection,
            @NonNull final EditorInfo outAttrs) {
        this(textView, inputConnection, outAttrs, new EmojiCompatDeleteHelper());
    }

    EmojiInputConnection(
            @NonNull final TextView textView,
            @NonNull final InputConnection inputConnection,
            @NonNull final EditorInfo outAttrs,
            @NonNull final EmojiCompatDeleteHelper emojiCompatDeleteHelper
    ) {
        super(inputConnection, false);
        mTextView = textView;
        mEmojiCompatDeleteHelper = emojiCompatDeleteHelper;
        mEmojiCompatDeleteHelper.updateEditorInfoAttrs(outAttrs);
    }

    @Override
    public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
        final boolean result = mEmojiCompatDeleteHelper.handleDeleteSurroundingText(
                this, getEditable(), beforeLength, afterLength, false /*inCodePoints*/);
        return result || super.deleteSurroundingText(beforeLength, afterLength);
    }

    @Override
    public boolean deleteSurroundingTextInCodePoints(final int beforeLength,
            final int afterLength) {
        final boolean result = mEmojiCompatDeleteHelper.handleDeleteSurroundingText(
                this, getEditable(), beforeLength, afterLength, true /*inCodePoints*/);
        return result || super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
    }

    private Editable getEditable() {
        return mTextView.getEditableText();
    }

    public static class EmojiCompatDeleteHelper {
        public boolean handleDeleteSurroundingText(
                @NonNull final InputConnection inputConnection,
                @NonNull final Editable editable,
                @IntRange(from = 0) final int beforeLength,
                @IntRange(from = 0) final int afterLength,
                final boolean inCodePoints) {
            return EmojiCompat.handleDeleteSurroundingText(inputConnection, editable,
                    beforeLength, afterLength, inCodePoints);
        }

        public void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
            if (EmojiCompat.isConfigured()) {
                EmojiCompat.get().updateEditorInfo(outAttrs);
            }
        }
    }
}