Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c486acc4 authored by Chris.CC Lee's avatar Chris.CC Lee Committed by Yohei Yukawa
Browse files

Add surrounding text to EditorInfo to speed up IME startup

Previously when IMEs want to get initial input text for advanced
prediction featues, it will take up to three IPC calls. Once we provide
the initial text in the EditorInfo object, it would be available right
after the input connection setup and no IPC overheads will be
introduced.

Bug: 122691437
Test: atest FrameworksCoreTests:TextViewTest
Test: atest FrameworksCoreTests:EditorInfoTest
Test: atest CtsWidgetTestCases:TextViewTest
Test: atest CtsInputMethodTestCases:EditorInfoTest
Change-Id: Ie04f2349b1157408aa8ed9044aea12ce99132cb4
parent 247901bb
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -53977,7 +53977,12 @@ package android.view.inputmethod {
    ctor public EditorInfo();
    method public int describeContents();
    method public void dump(android.util.Printer, String);
    method @Nullable public CharSequence getInitialSelectedText(int);
    method @Nullable public CharSequence getInitialTextAfterCursor(int, int);
    method @Nullable public CharSequence getInitialTextBeforeCursor(int, int);
    method public final void makeCompatible(int);
    method public void setInitialSurroundingSubText(@NonNull CharSequence, int);
    method public void setInitialSurroundingText(@NonNull CharSequence);
    method public void writeToParcel(android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.EditorInfo> CREATOR;
    field public static final int IME_ACTION_DONE = 6; // 0x6
+333 −0
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package android.view.inputmethod;

import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.os.Bundle;
@@ -28,7 +30,13 @@ import android.os.UserHandle;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Printer;
import android.view.View;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;

/**
@@ -491,6 +499,238 @@ public class EditorInfo implements InputType, Parcelable {
    @Nullable
    public UserHandle targetInputMethodUser = null;

    @IntDef({TrimPolicy.HEAD, TrimPolicy.TAIL})
    @Retention(RetentionPolicy.SOURCE)
    @interface TrimPolicy {
        int HEAD = 0;
        int TAIL = 1;
    }

    /**
     * The maximum length of initialSurroundingText. When the input text from
     * {@code setInitialSurroundingText(CharSequence)} is longer than this, trimming shall be
     * performed to keep memory efficiency.
     */
    @VisibleForTesting
    static final int MEMORY_EFFICIENT_TEXT_LENGTH = 2048;
    /**
     * When the input text is longer than {@code #MEMORY_EFFICIENT_TEXT_LENGTH}, we start trimming
     * the input text into three parts: BeforeCursor, Selection, and AfterCursor. We don't want to
     * trim the Selection but we also don't want it consumes all available space. Therefore, the
     * maximum acceptable Selection length is half of {@code #MEMORY_EFFICIENT_TEXT_LENGTH}.
     */
    @VisibleForTesting
    static final int MAX_INITIAL_SELECTION_LENGTH =  MEMORY_EFFICIENT_TEXT_LENGTH / 2;

    @NonNull
    private InitialSurroundingText mInitialSurroundingText = new InitialSurroundingText();

    /**
     * Editors may use this method to provide initial input text to IMEs. As the surrounding text
     * could be used to provide various input assistance, we recommend editors to provide the
     * complete initial input text in its {@link View#onCreateInputConnection(EditorInfo)} callback.
     * The supplied text will then be processed to serve {@code #getInitialTextBeforeCursor},
     * {@code #getInitialSelectedText}, and {@code #getInitialTextBeforeCursor}. System is allowed
     * to trim {@code sourceText} for various reasons while keeping the most valuable data to IMEs.
     *
     * <p><strong>Editor authors: </strong>Providing the initial input text helps reducing IPC calls
     * for IMEs to provide many modern features right after the connection setup. We recommend
     * calling this method in your implementation.
     *
     * @param sourceText The complete input text.
     */
    public void setInitialSurroundingText(@NonNull CharSequence sourceText) {
        setInitialSurroundingSubText(sourceText, /* subTextStart = */ 0);
    }

    /**
     * Editors may use this method to provide initial input text to IMEs. As the surrounding text
     * could be used to provide various input assistance, we recommend editors to provide the
     * complete initial input text in its {@link View#onCreateInputConnection(EditorInfo)} callback.
     * When trimming the input text is needed, call this method instead of
     * {@code setInitialSurroundingText(CharSequence)} and provide the trimmed position info. Always
     * try to include the selected text within {@code subText} to give the system best flexibility
     * to choose where and how to trim {@code subText} when necessary.
     *
     * @param subText The input text. When it was trimmed, {@code subTextStart} must be provided
     *                correctly.
     * @param subTextStart  The position that the input text got trimmed. For example, when the
     *                      editor wants to trim out the first 10 chars, subTextStart should be 10.
     */
    public void setInitialSurroundingSubText(@NonNull CharSequence subText, int subTextStart) {
        Preconditions.checkNotNull(subText);

        // Swap selection start and end if necessary.
        final int subTextSelStart = initialSelStart > initialSelEnd
                ? initialSelEnd - subTextStart : initialSelStart - subTextStart;
        final int subTextSelEnd = initialSelStart > initialSelEnd
                ? initialSelStart - subTextStart : initialSelEnd - subTextStart;

        final int subTextLength = subText.length();
        // Unknown or invalid selection.
        if (subTextStart < 0 || subTextSelStart < 0 || subTextSelEnd > subTextLength) {
            mInitialSurroundingText = new InitialSurroundingText();
            return;
        }

        // For privacy protection reason, we don't carry password inputs to IMEs.
        if (isPasswordInputType(inputType)) {
            mInitialSurroundingText = new InitialSurroundingText();
            return;
        }

        if (subTextLength <= MEMORY_EFFICIENT_TEXT_LENGTH) {
            mInitialSurroundingText = new InitialSurroundingText(subText, subTextSelStart,
                    subTextSelEnd);
            return;
        }

        // The input text is too long. Let's try to trim it reasonably. Fundamental rules are:
        // 1. Text before the cursor is the most important information to IMEs.
        // 2. Text after the cursor is the second important information to IMEs.
        // 3. Selected text is the least important information but it shall NEVER be truncated.
        //    When it is too long, just drop it.
        //
        // Source: <TextBeforeCursor><Selection><TextAfterCursor>
        // Possible results:
        // 1. <(maybeTrimmedAtHead)TextBeforeCursor><Selection><TextAfterCursor(maybeTrimmedAtTail)>
        // 2. <(maybeTrimmedAtHead)TextBeforeCursor><TextAfterCursor(maybeTrimmedAtTail)>
        //
        final int sourceSelLength = subTextSelEnd - subTextSelStart;
        // When the selected text is too long, drop it.
        final int newSelLength = (sourceSelLength > MAX_INITIAL_SELECTION_LENGTH)
                ? 0 : sourceSelLength;

        // Distribute rest of length quota to TextBeforeCursor and TextAfterCursor in 4:1 ratio.
        final int subTextBeforeCursorLength = subTextSelStart;
        final int subTextAfterCursorLength = subTextLength - subTextSelEnd;
        final int maxLengthMinusSelection = MEMORY_EFFICIENT_TEXT_LENGTH - newSelLength;
        final int possibleMaxBeforeCursorLength =
                Math.min(subTextBeforeCursorLength, (int) (0.8 * maxLengthMinusSelection));
        int newAfterCursorLength = Math.min(subTextAfterCursorLength,
                maxLengthMinusSelection - possibleMaxBeforeCursorLength);
        int newBeforeCursorLength = Math.min(subTextBeforeCursorLength,
                maxLengthMinusSelection - newAfterCursorLength);

        // As trimming may happen at the head of TextBeforeCursor, calculate new starting position.
        int newBeforeCursorHead = subTextBeforeCursorLength - newBeforeCursorLength;

        // We don't want to cut surrogate pairs in the middle. Exam that at the new head and tail.
        if (isCutOnSurrogate(subText,
                subTextSelStart - newBeforeCursorLength, TrimPolicy.HEAD)) {
            newBeforeCursorHead = newBeforeCursorHead + 1;
            newBeforeCursorLength = newBeforeCursorLength - 1;
        }
        if (isCutOnSurrogate(subText,
                subTextSelEnd + newAfterCursorLength - 1, TrimPolicy.TAIL)) {
            newAfterCursorLength = newAfterCursorLength - 1;
        }

        // Now we know where to trim, compose the initialSurroundingText.
        final int newTextLength = newBeforeCursorLength + newSelLength + newAfterCursorLength;
        CharSequence newInitialSurroundingText;
        if (newSelLength != sourceSelLength) {
            final CharSequence beforeCursor = subText.subSequence(newBeforeCursorHead,
                    newBeforeCursorHead + newBeforeCursorLength);

            final CharSequence afterCursor = subText.subSequence(subTextSelEnd,
                    subTextSelEnd + newAfterCursorLength);

            newInitialSurroundingText = TextUtils.concat(beforeCursor, afterCursor);
        } else {
            newInitialSurroundingText = subText
                    .subSequence(newBeforeCursorHead, newBeforeCursorHead + newTextLength);
        }

        // As trimming may happen at the head, adjust cursor position in the initialSurroundingText
        // obj.
        newBeforeCursorHead = 0;
        final int newSelHead = newBeforeCursorHead + newBeforeCursorLength;
        mInitialSurroundingText = new InitialSurroundingText(
                newInitialSurroundingText, newSelHead, newSelHead + newSelLength);
    }

    /**
     * Get <var>n</var> characters of text before the current cursor position. May be {@code null}
     * when the protocol is not supported.
     *
     * @param length The expected length of the text.
     * @param flags Supplies additional options controlling how the text is returned. May be
     * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}.
     * @return the text before the cursor position; the length of the returned text might be less
     * than <var>n</var>. When there is no text before the cursor, an empty string will be returned.
     * It could also be {@code null} when the editor or system could not support this protocol.
     */
    @Nullable
    public CharSequence getInitialTextBeforeCursor(int length, int flags) {
        return mInitialSurroundingText.getInitialTextBeforeCursor(length, flags);
    }

    /**
     * Gets the selected text, if any. May be {@code null} when no text is selected or the selected
     * text is way too long.
     *
     * @param flags Supplies additional options controlling how the text is returned. May be
     * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}.
     * @return the text that is currently selected, if any. It could be an empty string when there
     * is no text selected. When {@code null} is returned, the selected text might be too long or
     * this protocol is not supported.
     */
    @Nullable
    public CharSequence getInitialSelectedText(int flags) {
        // Swap selection start and end if necessary.
        final int correctedTextSelStart = initialSelStart > initialSelEnd
                ? initialSelEnd : initialSelStart;
        final int correctedTextSelEnd = initialSelStart > initialSelEnd
                ? initialSelStart : initialSelEnd;

        final int sourceSelLength = correctedTextSelEnd - correctedTextSelStart;
        if (initialSelStart < 0 || initialSelEnd < 0
                || mInitialSurroundingText.getSelectionLength() != sourceSelLength) {
            return null;
        }
        return mInitialSurroundingText.getInitialSelectedText(flags);
    }

    /**
     * Get <var>n</var> characters of text after the current cursor position. May be {@code null}
     * when the protocol is not supported.
     *
     * @param length The expected length of the text.
     * @param flags Supplies additional options controlling how the text is returned. May be
     * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}.
     * @return the text after the cursor position; the length of the returned text might be less
     * than <var>n</var>. When there is no text after the cursor, an empty string will be returned.
     * It could also be {@code null} when the editor or system could not support this protocol.
     */
    @Nullable
    public CharSequence getInitialTextAfterCursor(int length, int flags) {
        return mInitialSurroundingText.getInitialTextAfterCursor(length, flags);
    }

    private static boolean isCutOnSurrogate(CharSequence sourceText, int cutPosition,
            @TrimPolicy int policy) {
        switch (policy) {
            case TrimPolicy.HEAD:
                return Character.isLowSurrogate(sourceText.charAt(cutPosition));
            case TrimPolicy.TAIL:
                return Character.isHighSurrogate(sourceText.charAt(cutPosition));
            default:
                return false;
        }
    }

    private static boolean isPasswordInputType(int inputType) {
        final int variation =
                inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
        return variation
                == (TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD)
                || variation
                == (TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD)
                || variation
                == (TYPE_CLASS_NUMBER | TYPE_NUMBER_VARIATION_PASSWORD);
    }

    /**
     * Ensure that the data in this EditorInfo is compatible with an application
     * that was developed against the given target API version.  This can
@@ -573,6 +813,7 @@ public class EditorInfo implements InputType, Parcelable {
        dest.writeInt(fieldId);
        dest.writeString(fieldName);
        dest.writeBundle(extras);
        mInitialSurroundingText.writeToParcel(dest, flags);
        if (hintLocales != null) {
            hintLocales.writeToParcel(dest, flags);
        } else {
@@ -603,6 +844,9 @@ public class EditorInfo implements InputType, Parcelable {
                    res.fieldId = source.readInt();
                    res.fieldName = source.readString();
                    res.extras = source.readBundle();
                    InitialSurroundingText initialSurroundingText =
                            InitialSurroundingText.CREATOR.createFromParcel(source);
                    res.mInitialSurroundingText = initialSurroundingText;
                    LocaleList hintLocales = LocaleList.CREATOR.createFromParcel(source);
                    res.hintLocales = hintLocales.isEmpty() ? null : hintLocales;
                    res.contentMimeTypes = source.readStringArray();
@@ -619,4 +863,93 @@ public class EditorInfo implements InputType, Parcelable {
        return 0;
    }

    // TODO(b/148035211): Unit tests for this class
    static final class InitialSurroundingText implements Parcelable {
        @Nullable final CharSequence mSurroundingText;
        final int mSelectionHead;
        final int mSelectionEnd;

        InitialSurroundingText() {
            mSurroundingText = null;
            mSelectionHead = 0;
            mSelectionEnd = 0;
        }

        InitialSurroundingText(@Nullable CharSequence surroundingText, int selectionHead,
                int selectionEnd) {
            mSurroundingText = surroundingText;
            mSelectionHead = selectionHead;
            mSelectionEnd = selectionEnd;
        }

        @Nullable
        private CharSequence getInitialTextBeforeCursor(int n, int flags) {
            if (mSurroundingText == null) {
                return null;
            }

            final int length = Math.min(n, mSelectionHead);
            return ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
                    ? mSurroundingText.subSequence(mSelectionHead - length, mSelectionHead)
                    : TextUtils.substring(mSurroundingText, mSelectionHead - length,
                            mSelectionHead);
        }

        @Nullable
        private CharSequence getInitialSelectedText(int flags) {
            if (mSurroundingText == null) {
                return null;
            }

            return ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
                    ? mSurroundingText.subSequence(mSelectionHead, mSelectionEnd)
                    : TextUtils.substring(mSurroundingText, mSelectionHead, mSelectionEnd);
        }

        @Nullable
        private CharSequence getInitialTextAfterCursor(int n, int flags) {
            if (mSurroundingText == null) {
                return null;
            }

            final int length = Math.min(n, mSurroundingText.length() - mSelectionEnd);
            return ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
                    ? mSurroundingText.subSequence(mSelectionEnd, mSelectionEnd + length)
                    : TextUtils.substring(mSurroundingText, mSelectionEnd, mSelectionEnd + length);
        }

        private int getSelectionLength() {
            return mSelectionEnd - mSelectionHead;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            TextUtils.writeToParcel(mSurroundingText, dest, flags);
            dest.writeInt(mSelectionHead);
            dest.writeInt(mSelectionEnd);
        }

        public static final @android.annotation.NonNull Parcelable.Creator<InitialSurroundingText>
                CREATOR = new Parcelable.Creator<InitialSurroundingText>() {
                    @Override
                    public InitialSurroundingText createFromParcel(Parcel source) {
                        final CharSequence initialText =
                                TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
                        final int selectionHead = source.readInt();
                        final int selectionEnd = source.readInt();

                        return new InitialSurroundingText(initialText, selectionHead, selectionEnd);
                    }

                    @Override
                    public InitialSurroundingText[] newArray(int size) {
                        return new InitialSurroundingText[size];
                    }
                };
    }
}
+15 −3
Original line number Diff line number Diff line
@@ -158,7 +158,11 @@ public interface InputConnection {
     * trigger an IPC round-trip that will take some time. Assume this
     * method consumes a lot of time. Also, please keep in mind the
     * Editor may choose to return less characters than requested even
     * if they are available for performance reasons.</p>
     * if they are available for performance reasons. If you are using
     * this to get the initial text around the cursor, you may consider
     * using {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
     * {@link EditorInfo#getInitialSelectedText(int)}, and
     * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
     *
     * <p><strong>Editor authors:</strong> please be careful of race
     * conditions in implementing this call. An IME can make a change
@@ -196,7 +200,11 @@ public interface InputConnection {
     *
     * <p><strong>IME authors:</strong> please consider this will
     * trigger an IPC round-trip that will take some time. Assume this
     * method consumes a lot of time.</p>
     * method consumes a lot of time. If you are using this to get the
     * initial text around the cursor, you may consider using
     * {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
     * {@link EditorInfo#getInitialSelectedText(int)}, and
     * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
     *
     * <p><strong>Editor authors:</strong> please be careful of race
     * conditions in implementing this call. An IME can make a change
@@ -234,7 +242,11 @@ public interface InputConnection {
     *
     * <p><strong>IME authors:</strong> please consider this will
     * trigger an IPC round-trip that will take some time. Assume this
     * method consumes a lot of time.</p>
     * method consumes a lot of time. If you are using this to get the
     * initial text around the cursor, you may consider using
     * {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
     * {@link EditorInfo#getInitialSelectedText(int)}, and
     * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
     *
     * <p><strong>Editor authors:</strong> please be careful of race
     * conditions in implementing this call. An IME can make a change
+1 −0
Original line number Diff line number Diff line
@@ -8668,6 +8668,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                outAttrs.initialSelStart = getSelectionStart();
                outAttrs.initialSelEnd = getSelectionEnd();
                outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
                outAttrs.setInitialSurroundingText(mText);
                return ic;
            }
        }
+229 −0

File changed.

Preview size limit exceeded, changes collapsed.