15475b38328171a0841ae18074bd45380ec567e90Jean Chalard/*
25475b38328171a0841ae18074bd45380ec567e90Jean Chalard * Copyright (C) 2012 The Android Open Source Project
35475b38328171a0841ae18074bd45380ec567e90Jean Chalard *
48aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * Licensed under the Apache License, Version 2.0 (the "License");
58aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * you may not use this file except in compliance with the License.
68aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * You may obtain a copy of the License at
75475b38328171a0841ae18074bd45380ec567e90Jean Chalard *
88aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka *      http://www.apache.org/licenses/LICENSE-2.0
95475b38328171a0841ae18074bd45380ec567e90Jean Chalard *
105475b38328171a0841ae18074bd45380ec567e90Jean Chalard * Unless required by applicable law or agreed to in writing, software
118aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * distributed under the License is distributed on an "AS IS" BASIS,
128aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
138aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * See the License for the specific language governing permissions and
148aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * limitations under the License.
155475b38328171a0841ae18074bd45380ec567e90Jean Chalard */
165475b38328171a0841ae18074bd45380ec567e90Jean Chalard
175475b38328171a0841ae18074bd45380ec567e90Jean Chalardpackage com.android.inputmethod.latin;
185475b38328171a0841ae18074bd45380ec567e90Jean Chalard
19f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalardimport android.inputmethodservice.InputMethodService;
209273f3832b51f5d23d86df624600381ed6d6585fJean Chalardimport android.os.Build;
21c92c883fdf2287b49392692fa2e8d109dc26f785David Fadenimport android.os.Bundle;
2273aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovicimport android.os.SystemClock;
234a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawaimport android.text.SpannableStringBuilder;
24bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalardimport android.text.TextUtils;
25f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovicimport android.text.style.CharacterStyle;
265475b38328171a0841ae18074bd45380ec567e90Jean Chalardimport android.util.Log;
275475b38328171a0841ae18074bd45380ec567e90Jean Chalardimport android.view.KeyEvent;
285475b38328171a0841ae18074bd45380ec567e90Jean Chalardimport android.view.inputmethod.CompletionInfo;
295475b38328171a0841ae18074bd45380ec567e90Jean Chalardimport android.view.inputmethod.CorrectionInfo;
3002308bec632a5df23325c916bffec5def16b22b4Jean Chalardimport android.view.inputmethod.ExtractedText;
3102308bec632a5df23325c916bffec5def16b22b4Jean Chalardimport android.view.inputmethod.ExtractedTextRequest;
325475b38328171a0841ae18074bd45380ec567e90Jean Chalardimport android.view.inputmethod.InputConnection;
3329200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawaimport android.view.inputmethod.InputMethodManager;
345475b38328171a0841ae18074bd45380ec567e90Jean Chalard
3529200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawaimport com.android.inputmethod.compat.InputConnectionCompatUtils;
369342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport com.android.inputmethod.latin.common.Constants;
37f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovicimport com.android.inputmethod.latin.common.UnicodeSurrogate;
384beeb9253a06482299e0c67467531d30436a02fcJean Chalardimport com.android.inputmethod.latin.common.StringUtils;
39c92c883fdf2287b49392692fa2e8d109dc26f785David Fadenimport com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer;
40494e2d6c17cdbf27615a2fbc02b12d2562bf7cd3Tadashi G. Takaokaimport com.android.inputmethod.latin.settings.SpacingAndPunctuations;
41e28eba5074664d5716b8e58b8d0a235746b261ebKen Wakasaimport com.android.inputmethod.latin.utils.CapsModeUtils;
4203118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasaimport com.android.inputmethod.latin.utils.DebugLogUtils;
43bb0eca57054758ef17b032d2654c1fc5f6b32101Keisuke Kuroyanagiimport com.android.inputmethod.latin.utils.NgramContextUtils;
44292deb632cbab232334190e68d29184094d6d51bJean Chalardimport com.android.inputmethod.latin.utils.ScriptUtils;
45f56b82f80961a511765df206eee36229cbee6ed8Ken Wakasaimport com.android.inputmethod.latin.utils.SpannableStringUtils;
4673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovicimport com.android.inputmethod.latin.utils.StatsUtils;
47675bcf191c3cdb5ba5af70efb9357ffceb389c2eJean Chalardimport com.android.inputmethod.latin.utils.TextRange;
48bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard
49912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyangimport java.util.concurrent.TimeUnit;
50912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang
51d3a4c5132422b189c8dbb94dbbe84a9b9761b0a8Tadashi G. Takaokaimport javax.annotation.Nonnull;
52db96122787a57632136566e2448c21aa96879955Mohammadinamul Sheikimport javax.annotation.Nullable;
53d3a4c5132422b189c8dbb94dbbe84a9b9761b0a8Tadashi G. Takaoka
545475b38328171a0841ae18074bd45380ec567e90Jean Chalard/**
5528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard * Enrichment class for InputConnection to simplify interaction and add functionality.
5628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard *
5728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
5828d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
5928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard * all the time to find out what text is in the buffer, when we need it to determine caps mode
6028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard * for example.
615475b38328171a0841ae18074bd45380ec567e90Jean Chalard */
62c92c883fdf2287b49392692fa2e8d109dc26f785David Fadenpublic final class RichInputConnection implements PrivateCommandPerformer {
630232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic    private static final String TAG = "RichInputConnection";
645475b38328171a0841ae18074bd45380ec567e90Jean Chalard    private static final boolean DBG = false;
6528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    private static final boolean DEBUG_PREVIOUS_TEXT = false;
66574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard    private static final boolean DEBUG_BATCH_NESTING = false;
670232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic    private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40;
680232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic    private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40;
6902308bec632a5df23325c916bffec5def16b22b4Jean Chalard    private static final int INVALID_CURSOR_POSITION = -1;
70912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang
71912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    /**
72563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic     * The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter
73563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic     * the {@link #hasSlowInputConnection} state.
74912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     */
75563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic    private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000;
76563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic    /**
77563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic     * The amount of time a {@link #getTextBeforeCursor} or {@link #getTextAfterCursor} call needs
78563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic     * to take for the keyboard to enter the {@link #hasSlowInputConnection} state.
79563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic     */
80563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic    private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS = 200;
81563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic
8273aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0;
8373aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1;
8473aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2;
8573aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private static final int OPERATION_RELOAD_TEXT_CACHE = 3;
8673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private static final String[] OPERATION_NAMES = new String[] {
8773aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            "GET_TEXT_BEFORE_CURSOR",
8873aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            "GET_TEXT_AFTER_CURSOR",
8973aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            "GET_WORD_RANGE_AT_CURSOR",
9073aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            "RELOAD_TEXT_CACHE"};
9102308bec632a5df23325c916bffec5def16b22b4Jean Chalard
9228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    /**
93563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic     * The amount of time the keyboard will persist in the {@link #hasSlowInputConnection} state
94912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     * after observing a slow InputConnection event.
95912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     */
96912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    private static final long SLOW_INPUTCONNECTION_PERSIST_MS = TimeUnit.MINUTES.toMillis(10);
97912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang
98912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    /**
992fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * This variable contains an expected value for the selection start position. This is where the
1002fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * cursor or selection start may end up after all the keyboard-triggered updates have passed. We
1012fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * keep this to compare it to the actual selection start to guess whether the move was caused by
1022fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * a keyboard command or not.
1032fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * It's not really the selection start position: the selection start may not be there yet, and
1042fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * in some cases, it may never arrive there.
10528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard     */
1062fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points
1072fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    /**
1082fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * The expected selection end.  Only differs from mExpectedSelStart if a non-empty selection is
1092fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * expected.  The same caveats as mExpectedSelStart apply.
1102fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     */
1112fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points
11228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    /**
11328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard     * This contains the committed text immediately preceding the cursor and the composing
1140232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic     * text, if any. It is refreshed when the cursor moves by calling upon the TextView.
11528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard     */
116740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard    private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
11728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    /**
11828d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard     * This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
11928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard     */
120740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard    private final StringBuilder mComposingText = new StringBuilder();
12128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard
1224a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa    /**
123c71e4d65343bf782d80ce508786befa6ee3261cfDan Zivkovic     * This variable is a temporary object used in {@link #commitText(CharSequence,int)}
124c71e4d65343bf782d80ce508786befa6ee3261cfDan Zivkovic     * to avoid object creation.
1254a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa     */
1264a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa    private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
1274a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa
128f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard    private final InputMethodService mParent;
129563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic    private InputConnection mIC;
130563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic    private int mNestLevel;
131edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic
132912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    /**
133912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     * The timestamp of the last slow InputConnection operation
134912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     */
13596640a110b97bbb48bc45ef4671d3927952e6cbaTom Ouyang    private long mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS;
136912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang
137f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard    public RichInputConnection(final InputMethodService parent) {
138f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mParent = parent;
1395475b38328171a0841ae18074bd45380ec567e90Jean Chalard        mIC = null;
1405475b38328171a0841ae18074bd45380ec567e90Jean Chalard        mNestLevel = 0;
1415475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
1425475b38328171a0841ae18074bd45380ec567e90Jean Chalard
143edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic    public boolean isConnected() {
144edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        return mIC != null;
145edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic    }
146edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic
147912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    /**
148912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     * Returns whether or not the underlying InputConnection is slow. When true, we want to avoid
149912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     * calling InputConnection methods that trigger an IPC round-trip (e.g., getTextAfterCursor).
150912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang     */
151912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    public boolean hasSlowInputConnection() {
15296640a110b97bbb48bc45ef4671d3927952e6cbaTom Ouyang        return (SystemClock.uptimeMillis() - mLastSlowInputConnectionTime)
153912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang                        <= SLOW_INPUTCONNECTION_PERSIST_MS;
154912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    }
155912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang
156912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    public void onStartInput() {
15796640a110b97bbb48bc45ef4671d3927952e6cbaTom Ouyang        mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS;
158912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    }
159912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang
16028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    private void checkConsistencyForDebug() {
16128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        final ExtractedTextRequest r = new ExtractedTextRequest();
16228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        r.hintMaxChars = 0;
16328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        r.hintMaxLines = 0;
16428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        r.token = 1;
16528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        r.flags = 0;
16628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        final ExtractedText et = mIC.getExtractedText(r, 0);
167f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
168f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard                0);
1696a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText)
17028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                .append(mComposingText);
17128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (null == et || null == beforeCursor) return;
17228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        final int actualLength = Math.min(beforeCursor.length(), internal.length());
17328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (internal.length() > actualLength) {
17428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            internal.delete(0, internal.length() - actualLength);
17528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        }
17628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
17728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                : beforeCursor.subSequence(beforeCursor.length() - actualLength,
17828d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                        beforeCursor.length()).toString();
1792fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (et.selectionStart != mExpectedSelStart
18028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                || !(reference.equals(internal.toString()))) {
1812fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            final String context = "Expected selection start = " + mExpectedSelStart
1822fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                    + "\nActual selection start = " + et.selectionStart
18328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    + "\nExpected text = " + internal.length() + " " + internal
18428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    + "\nActual text = " + reference.length() + " " + reference;
18528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
18628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        } else {
18703118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa            Log.e(TAG, DebugLogUtils.getStackTrace(2));
1882fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart);
18928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        }
19028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    }
19128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard
192f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard    public void beginBatchEdit() {
1935475b38328171a0841ae18074bd45380ec567e90Jean Chalard        if (++mNestLevel == 1) {
194f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard            mIC = mParent.getCurrentInputConnection();
195edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic            if (isConnected()) {
19694e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge                mIC.beginBatchEdit();
19794e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            }
1985475b38328171a0841ae18074bd45380ec567e90Jean Chalard        } else {
1995475b38328171a0841ae18074bd45380ec567e90Jean Chalard            if (DBG) {
2005475b38328171a0841ae18074bd45380ec567e90Jean Chalard                throw new RuntimeException("Nest level too deep");
2015475b38328171a0841ae18074bd45380ec567e90Jean Chalard            }
2025f00fe09e9a611b647592188316e5999465df4d3Tadashi G. Takaoka            Log.e(TAG, "Nest level too deep : " + mNestLevel);
2035475b38328171a0841ae18074bd45380ec567e90Jean Chalard        }
204574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
20528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
2065475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
20728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard
2085475b38328171a0841ae18074bd45380ec567e90Jean Chalard    public void endBatchEdit() {
2095475b38328171a0841ae18074bd45380ec567e90Jean Chalard        if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
210edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (--mNestLevel == 0 && isConnected()) {
21194e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            mIC.endBatchEdit();
21294e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
21328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
21428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard    }
21528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard
216f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard    /**
217f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * Reset the cached text and retrieve it again from the editor.
218f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     *
219f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * This should be called when the cursor moved. It's possible that we can't connect to
220f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * the application when doing this; notably, this happens sometimes during rotation, probably
221f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * because of a race condition in the framework. In this case, we just can't retrieve the
222f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * data, so we empty the cache and note that we don't know the new cursor position, and we
223f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * return false so that the caller knows about this and can retry later.
224f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     *
2252fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param newSelStart the new position of the selection start, as received from the system.
2262fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param newSelEnd the new position of the selection end, as received from the system.
2272fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param shouldFinishComposition whether we should finish the composition in progress.
228f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     * @return true if we were able to connect to the editor successfully, false otherwise. When
229f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     *   this method returns false, the caches could not be correctly refreshed so they were only
230f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     *   reset: the caller should try again later to return to normal operation.
231f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard     */
2322fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart,
2332fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            final int newSelEnd, final boolean shouldFinishComposition) {
2342fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelStart = newSelStart;
2352fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelEnd = newSelEnd;
23628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mComposingText.setLength(0);
2372fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        final boolean didReloadTextSuccessfully = reloadTextCache();
2382fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (!didReloadTextSuccessfully) {
2392fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            Log.d(TAG, "Will try to retrieve text later.");
2402fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            return false;
2412fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        }
242edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected() && shouldFinishComposition) {
2432fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            mIC.finishComposingText();
2442fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        }
2452fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        return true;
2462fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    }
2472fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa
2482fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    /**
2492fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * Reload the cached text from the InputConnection.
2502fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     *
2512fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @return true if successful
2522fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     */
2532fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    private boolean reloadTextCache() {
25428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mCommittedTextBeforeComposingText.setLength(0);
255f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        mIC = mParent.getCurrentInputConnection();
256f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        // Call upon the inputconnection directly since our own method is using the cache, and
257f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        // we want to refresh it.
25873aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        final CharSequence textBeforeCursor = getTextBeforeCursorAndDetectLaggyConnection(
25973aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                OPERATION_RELOAD_TEXT_CACHE,
260563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS,
26173aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                Constants.EDITOR_CONTENTS_CACHE_SIZE,
26273aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                0 /* flags */);
263f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        if (null == textBeforeCursor) {
264f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard            // For some reason the app thinks we are not connected to it. This looks like a
265f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard            // framework bug... Fall back to ground state and return false.
2662fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            mExpectedSelStart = INVALID_CURSOR_POSITION;
2672fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            mExpectedSelEnd = INVALID_CURSOR_POSITION;
2682fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            Log.e(TAG, "Unable to connect to the editor to retrieve text.");
269f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard            return false;
270f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        }
271f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        mCommittedTextBeforeComposingText.append(textBeforeCursor);
272f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard        return true;
2735475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
2745475b38328171a0841ae18074bd45380ec567e90Jean Chalard
2759d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard    private void checkBatchEdit() {
2769d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard        if (mNestLevel != 1) {
2779d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard            // TODO: exception instead
2789d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard            Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
27903118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa            Log.e(TAG, DebugLogUtils.getStackTrace(4));
2809d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard        }
2819d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard    }
2829d71748ba48dbc8793f3e1ecddf5fd31b8e59613Jean Chalard
2835475b38328171a0841ae18074bd45380ec567e90Jean Chalard    public void finishComposingText() {
284574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
28528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
286fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // TODO: this is not correct! The cursor is not necessarily after the composing text.
287fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // In the practice right now this is only called when input ends so it will be reset so
288fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // it works, but it's wrong and should be fixed.
28928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mCommittedTextBeforeComposingText.append(mComposingText);
29028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mComposingText.setLength(0);
291edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
29294e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            mIC.finishComposingText();
29394e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
2945475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
2955475b38328171a0841ae18074bd45380ec567e90Jean Chalard
2964a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa    /**
297c71e4d65343bf782d80ce508786befa6ee3261cfDan Zivkovic     * Calls {@link InputConnection#commitText(CharSequence, int)}.
298c71e4d65343bf782d80ce508786befa6ee3261cfDan Zivkovic     *
2994a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa     * @param text The text to commit. This may include styles.
3004a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa     * @param newCursorPosition The new cursor position around the text.
3014a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa     */
3024a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa    public void commitText(final CharSequence text, final int newCursorPosition) {
303574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
30428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
30528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mCommittedTextBeforeComposingText.append(text);
306fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // TODO: the following is exceedingly error-prone. Right now when the cursor is in the
307fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // middle of the composing word mComposingText only holds the part of the composing text
308fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // that is before the cursor, so this actually works, but it's terribly confusing. Fix this.
3092fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelStart += text.length() - mComposingText.length();
3102fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelEnd = mExpectedSelStart;
31128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mComposingText.setLength(0);
312edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
313f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic            mTempObjectForCommitText.clear();
314f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic            mTempObjectForCommitText.append(text);
315f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic            final CharacterStyle[] spans = mTempObjectForCommitText.getSpans(
316f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                    0, text.length(), CharacterStyle.class);
317f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic            for (final CharacterStyle span : spans) {
318f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                final int spanStart = mTempObjectForCommitText.getSpanStart(span);
319f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                final int spanEnd = mTempObjectForCommitText.getSpanEnd(span);
320f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                final int spanFlags = mTempObjectForCommitText.getSpanFlags(span);
321f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                // We have to adjust the end of the span to include an additional character.
322f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                // This is to avoid splitting a unicode surrogate pair.
323f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                // See com.android.inputmethod.latin.common.Constants.UnicodeSurrogate
324f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                // See https://b.corp.google.com/issues/19255233
325f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                if (0 < spanEnd && spanEnd < mTempObjectForCommitText.length()) {
326f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                    final char spanEndChar = mTempObjectForCommitText.charAt(spanEnd - 1);
327f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                    final char nextChar = mTempObjectForCommitText.charAt(spanEnd);
328f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                    if (UnicodeSurrogate.isLowSurrogate(spanEndChar)
329f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                            && UnicodeSurrogate.isHighSurrogate(nextChar)) {
330f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                        mTempObjectForCommitText.setSpan(span, spanStart, spanEnd + 1, spanFlags);
331f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                    }
332f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic                }
333f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic            }
334f3c319fb8ac29448c491af95261a4ce01b64a59cDan Zivkovic            mIC.commitText(mTempObjectForCommitText, newCursorPosition);
3354a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa        }
3364a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa    }
3374a71d2c48021351e10bd61d5ee007533651da627Yohei Yukawa
338db96122787a57632136566e2448c21aa96879955Mohammadinamul Sheik    @Nullable
3392995abe7aadd483aa57a9b088740d46ac07bbe46Jean Chalard    public CharSequence getSelectedText(final int flags) {
340edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        return isConnected() ?  mIC.getSelectedText(flags) : null;
3412995abe7aadd483aa57a9b088740d46ac07bbe46Jean Chalard    }
3422995abe7aadd483aa57a9b088740d46ac07bbe46Jean Chalard
343f87e8f7ec1efb93398d909c67468d716b0248fe7Tadashi G. Takaoka    public boolean canDeleteCharacters() {
3442fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        return mExpectedSelStart > 0;
345f87e8f7ec1efb93398d909c67468d716b0248fe7Tadashi G. Takaoka    }
346f87e8f7ec1efb93398d909c67468d716b0248fe7Tadashi G. Takaoka
34790a91272447fd64bc54e06f08f45b11c45930767Jean Chalard    /**
34890a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * Gets the caps modes we should be in after this specific string.
34990a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     *
35090a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
35190a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * This method also supports faking an additional space after the string passed in argument,
35290a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * to support cases where a space will be added automatically, like in phantom space
35390a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * state for example.
35490a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * Note that for English, we are using American typography rules (which are not specific to
35590a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * American English, it's just the most common set of rules for English).
35690a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     *
35790a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * @param inputType a mask of the caps modes to test for.
3586a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka     * @param spacingAndPunctuations the values of the settings to use for locale and separators.
35990a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * @param hasSpaceBefore if we should consider there should be a space after the string.
36090a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     * @return the caps modes that should be on as a set of bits
36190a91272447fd64bc54e06f08f45b11c45930767Jean Chalard     */
3626a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka    public int getCursorCapsMode(final int inputType,
3636a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka            final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) {
364f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mIC = mParent.getCurrentInputConnection();
365edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (!isConnected()) {
366edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic            return Constants.TextUtils.CAP_MODE_OFF;
367edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        }
368d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard        if (!TextUtils.isEmpty(mComposingText)) {
369d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard            if (hasSpaceBefore) {
370d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard                // If we have some composing text and a space before, then we should have
371d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard                // MODE_CHARACTERS and MODE_WORDS on.
372d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard                return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
373d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard            }
3745f00fe09e9a611b647592188316e5999465df4d3Tadashi G. Takaoka            // We have some composing text - we should be in MODE_CHARACTERS only.
3755f00fe09e9a611b647592188316e5999465df4d3Tadashi G. Takaoka            return TextUtils.CAP_MODE_CHARACTERS & inputType;
376d461bfd6601dfb6d4a4f78be0ff129597af895ffJean Chalard        }
3779d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // TODO: this will generally work, but there may be cases where the buffer contains SOME
3789d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // information but not enough to determine the caps mode accurately. This may happen after
3799d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
3809d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // getCapsMode should be updated to be able to return a "not enough info" result so that
3819d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // we can get more context only when needed.
3822fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) {
3832fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            if (!reloadTextCache()) {
3842fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                Log.w(TAG, "Unable to connect to the editor. "
3852fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                        + "Setting caps mode without knowing text.");
3868f02f1a118d36a1c4143b62f7e20727b2d2bf363Jean Chalard            }
3879d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        }
3889d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // This never calls InputConnection#getCapsMode - in fact, it's a static method that
3899d1c73ffd88cd1bfef3de048b0b3a9a7dfbcfa70Jean Chalard        // never blocks or initiates IPC.
3904097a20b36c7a6678f2b7486d587b7c43852c2e1Jean Chalard        // TODO: don't call #toString() here. Instead, all accesses to
3914097a20b36c7a6678f2b7486d587b7c43852c2e1Jean Chalard        // mCommittedTextBeforeComposingText should be done on the main thread.
3924097a20b36c7a6678f2b7486d587b7c43852c2e1Jean Chalard        return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText.toString(), inputType,
3936a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka                spacingAndPunctuations, hasSpaceBefore);
3945475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
3955475b38328171a0841ae18074bd45380ec567e90Jean Chalard
396e91d495c53a2606962159cfddada2b7a5e206c4cJean Chalard    public int getCodePointBeforeCursor() {
3976a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        final int length = mCommittedTextBeforeComposingText.length();
3986a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        if (length < 1) return Constants.NOT_A_CODE;
3996a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        return Character.codePointBefore(mCommittedTextBeforeComposingText, length);
400e91d495c53a2606962159cfddada2b7a5e206c4cJean Chalard    }
401e91d495c53a2606962159cfddada2b7a5e206c4cJean Chalard
4028c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard    public CharSequence getTextBeforeCursor(final int n, final int flags) {
4038c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard        final int cachedLength =
4048c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard                mCommittedTextBeforeComposingText.length() + mComposingText.length();
4058c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard        // If we have enough characters to satisfy the request, or if we have all characters in
4068c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard        // the text field, then we can return the cached version right away.
4073a9b2430a56cb2d774070bd8ecd661d0dfb82484Jean Chalard        // However, if we don't have an expected cursor position, then we should always
4083a9b2430a56cb2d774070bd8ecd661d0dfb82484Jean Chalard        // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to
4093a9b2430a56cb2d774070bd8ecd661d0dfb82484Jean Chalard        // test for this explicitly)
4102fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (INVALID_CURSOR_POSITION != mExpectedSelStart
4112fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                && (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
4128c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard            final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
4138a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // We call #toString() here to create a temporary object.
4148a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // In some situations, this method is called on a worker thread, and it's possible
4158a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // the main thread touches the contents of mComposingText while this worker thread
4168a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // is suspended, because mComposingText is a StringBuilder. This may lead to crashes,
4178a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // so we call #toString() on it. That will result in the return value being strictly
4188a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // speaking wrong, but since this is used for basing bigram probability off, and
4198a1675379e03bd7830d35212fd987346927068a9Jean Chalard            // it's only going to matter for one getSuggestions call, it's fine in the practice.
4208a1675379e03bd7830d35212fd987346927068a9Jean Chalard            s.append(mComposingText.toString());
4218c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard            if (s.length() > n) {
4228c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard                s.delete(0, s.length() - n);
4238c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard            }
4248c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard            return s;
4258c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard        }
42673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        return getTextBeforeCursorAndDetectLaggyConnection(
427563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                OPERATION_GET_TEXT_BEFORE_CURSOR,
428563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
429563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                n, flags);
43073aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    }
43173aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic
43273aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private CharSequence getTextBeforeCursorAndDetectLaggyConnection(
433563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic            final int operation, final long timeout, final int n, final int flags) {
434f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mIC = mParent.getCurrentInputConnection();
43573aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        if (!isConnected()) {
43673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            return null;
43773aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        }
438912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang        final long startTime = SystemClock.uptimeMillis();
43973aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        final CharSequence result = mIC.getTextBeforeCursor(n, flags);
440563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic        detectLaggyConnection(operation, timeout, startTime);
44173aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        return result;
4425475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
4435475b38328171a0841ae18074bd45380ec567e90Jean Chalard
4448c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard    public CharSequence getTextAfterCursor(final int n, final int flags) {
44573aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        return getTextAfterCursorAndDetectLaggyConnection(
446563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                OPERATION_GET_TEXT_AFTER_CURSOR,
447563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
448563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                n, flags);
44973aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    }
45073aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic
45173aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    private CharSequence getTextAfterCursorAndDetectLaggyConnection(
452563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic            final int operation, final long timeout, final int n, final int flags) {
453f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mIC = mParent.getCurrentInputConnection();
45473aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        if (!isConnected()) {
45573aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            return null;
45673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        }
45773aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        final long startTime = SystemClock.uptimeMillis();
45873aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        final CharSequence result = mIC.getTextAfterCursor(n, flags);
459563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic        detectLaggyConnection(operation, timeout, startTime);
46073aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        return result;
46173aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic    }
46273aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic
463563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic    private void detectLaggyConnection(final int operation, final long timeout, final long startTime) {
46473aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        final long duration = SystemClock.uptimeMillis() - startTime;
465563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic        if (duration >= timeout) {
46673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            final String operationName = OPERATION_NAMES[operation];
46773aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms.");
46873aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic            StatsUtils.onInputConnectionLaggy(operation, duration);
469912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang            mLastSlowInputConnectionTime = SystemClock.uptimeMillis();
47073aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        }
4715475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
4725475b38328171a0841ae18074bd45380ec567e90Jean Chalard
4730232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic    public void deleteTextBeforeCursor(final int beforeLength) {
474574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
475fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // TODO: the following is incorrect if the cursor is not immediately after the composition.
476fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // Right now we never come here in this case because we reset the composing state before we
477fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard        // come here in this case, but we need to fix this.
4788c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard        final int remainingChars = mComposingText.length() - beforeLength;
47928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (remainingChars >= 0) {
48028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            mComposingText.setLength(remainingChars);
48128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        } else {
48228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            mComposingText.setLength(0);
48328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            // Never cut under 0
48428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            final int len = Math.max(mCommittedTextBeforeComposingText.length()
48528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    + remainingChars, 0);
48628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            mCommittedTextBeforeComposingText.setLength(len);
48728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        }
4882fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (mExpectedSelStart > beforeLength) {
4892fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            mExpectedSelStart -= beforeLength;
4902fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            mExpectedSelEnd -= beforeLength;
49128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        } else {
4922fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            // There are fewer characters before the cursor in the buffer than we are being asked to
493e93a7232d17caffdca2ab3f62881ab0ae31f1cd5Jean Chalard            // delete. Only delete what is there, and update the end with the amount deleted.
4942fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            mExpectedSelEnd -= mExpectedSelStart;
495e93a7232d17caffdca2ab3f62881ab0ae31f1cd5Jean Chalard            mExpectedSelStart = 0;
49628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        }
497edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
4980232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic            mIC.deleteSurroundingText(beforeLength, 0);
49994e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
50028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
5015475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
5025475b38328171a0841ae18074bd45380ec567e90Jean Chalard
5035475b38328171a0841ae18074bd45380ec567e90Jean Chalard    public void performEditorAction(final int actionId) {
504f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mIC = mParent.getCurrentInputConnection();
505edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
50694e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            mIC.performEditorAction(actionId);
50794e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
5085475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
5095475b38328171a0841ae18074bd45380ec567e90Jean Chalard
5105475b38328171a0841ae18074bd45380ec567e90Jean Chalard    public void sendKeyEvent(final KeyEvent keyEvent) {
511574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
51228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
51328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
514b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            // This method is only called for enter or backspace when speaking to old applications
515b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            // (target SDK <= 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), or for digits.
51628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            // When talking to new applications we never use this method because it's inherently
51728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            // racy and has unpredictable results, but for backward compatibility we continue
51828d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            // sending the key events for only Enter and Backspace because some applications
51928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            // mistakenly catch them to do some stuff.
52028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            switch (keyEvent.getKeyCode()) {
521b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            case KeyEvent.KEYCODE_ENTER:
522b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                mCommittedTextBeforeComposingText.append("\n");
5232fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                mExpectedSelStart += 1;
5242fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                mExpectedSelEnd = mExpectedSelStart;
525b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                break;
526b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            case KeyEvent.KEYCODE_DEL:
527b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                if (0 == mComposingText.length()) {
528b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                    if (mCommittedTextBeforeComposingText.length() > 0) {
529b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                        mCommittedTextBeforeComposingText.delete(
530b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                                mCommittedTextBeforeComposingText.length() - 1,
531b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                                mCommittedTextBeforeComposingText.length());
53228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    }
533b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                } else {
534b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                    mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
535b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                }
5362fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) {
5372fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                    // TODO: Handle surrogate pairs.
5382fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                    mExpectedSelStart -= 1;
5392fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                }
5402fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                mExpectedSelEnd = mExpectedSelStart;
541b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                break;
542b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            case KeyEvent.KEYCODE_UNKNOWN:
543b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                if (null != keyEvent.getCharacters()) {
544b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                    mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
5452fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                    mExpectedSelStart += keyEvent.getCharacters().length();
5462fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                    mExpectedSelEnd = mExpectedSelStart;
547b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                }
548b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                break;
549b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            default:
5502fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar());
551b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                mCommittedTextBeforeComposingText.append(text);
5522fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                mExpectedSelStart += text.length();
5532fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                mExpectedSelEnd = mExpectedSelStart;
554b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa                break;
55528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            }
55628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        }
557edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
55894e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            mIC.sendKeyEvent(keyEvent);
55994e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
5605475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
5615475b38328171a0841ae18074bd45380ec567e90Jean Chalard
56218d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard    public void setComposingRegion(final int start, final int end) {
56318d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
56418d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
56518d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard        final CharSequence textBeforeCursor =
566f1d8aa46f9172c2d8864d0d2161aa8220d036cc9Jean Chalard                getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0);
56718d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard        mCommittedTextBeforeComposingText.setLength(0);
568740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard        if (!TextUtils.isEmpty(textBeforeCursor)) {
569fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // The cursor is not necessarily at the end of the composing text, but we have its
570fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start
571fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // of the text, so we should use mExpectedSelStart. In other words, the composing
572fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor
573740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard            final int indexOfStartOfComposingText =
574fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard                    Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0);
575740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard            mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText,
576740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard                    textBeforeCursor.length()));
577740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard            mCommittedTextBeforeComposingText.append(
578740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard                    textBeforeCursor.subSequence(0, indexOfStartOfComposingText));
579740da0d8d0b29297fba75f4b63ccff28fc1f4a14Jean Chalard        }
580edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
58118d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard            mIC.setComposingRegion(start, end);
58218d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard        }
58318d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard    }
58418d688c94bb8e1e26de2d12445cb3096c6126f75Jean Chalard
5858c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard    public void setComposingText(final CharSequence text, final int newCursorPosition) {
586574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
58728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
5882fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelStart += text.length() - mComposingText.length();
5892fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelEnd = mExpectedSelStart;
59028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mComposingText.setLength(0);
59128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mComposingText.append(text);
5922fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        // TODO: support values of newCursorPosition != 1. At this time, this is never called with
5932fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        // newCursorPosition != 1.
594edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
5958c6537edbd9e80b2d7169ecd31b1f0efbd1f9f20Jean Chalard            mIC.setComposingText(text, newCursorPosition);
59694e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
59728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
5985475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
5995475b38328171a0841ae18074bd45380ec567e90Jean Chalard
6002fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    /**
6012fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * Set the selection of the text editor.
6022fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     *
6032fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * Calls through to {@link InputConnection#setSelection(int, int)}.
6042fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     *
6052fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param start the character index where the selection should start.
6062fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param end the character index where the selection should end.
607f19745728e7231ffc8d7774b32821f31473ce1beJean Chalard     * @return Returns true on success, false on failure: either the input connection is no longer
608f19745728e7231ffc8d7774b32821f31473ce1beJean Chalard     * valid when setting the selection or when retrieving the text cache at that point, or
609f19745728e7231ffc8d7774b32821f31473ce1beJean Chalard     * invalid arguments were passed.
6102fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     */
6112fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    public boolean setSelection(final int start, final int end) {
612574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
61328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
614f19745728e7231ffc8d7774b32821f31473ce1beJean Chalard        if (start < 0 || end < 0) {
615f19745728e7231ffc8d7774b32821f31473ce1beJean Chalard            return false;
616f19745728e7231ffc8d7774b32821f31473ce1beJean Chalard        }
6172fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelStart = start;
6182fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelEnd = end;
619edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
6202fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            final boolean isIcValid = mIC.setSelection(start, end);
6212fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            if (!isIcValid) {
6222fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                return false;
6232fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            }
62494e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
6252fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        return reloadTextCache();
6265475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
6275475b38328171a0841ae18074bd45380ec567e90Jean Chalard
6285475b38328171a0841ae18074bd45380ec567e90Jean Chalard    public void commitCorrection(final CorrectionInfo correctionInfo) {
629574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
63028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
63128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        // This has no effect on the text field and does not change its content. It only makes
63228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        // TextView flash the text for a second based on indices contained in the argument.
633edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
63494e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            mIC.commitCorrection(correctionInfo);
63594e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
63628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
6375475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
6385475b38328171a0841ae18074bd45380ec567e90Jean Chalard
6395475b38328171a0841ae18074bd45380ec567e90Jean Chalard    public void commitCompletion(final CompletionInfo completionInfo) {
640574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
64128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
64202ce3dc2d11aba2b521f85223af1f870207b81dcJean Chalard        CharSequence text = completionInfo.getText();
64302ce3dc2d11aba2b521f85223af1f870207b81dcJean Chalard        // text should never be null, but just in case, it's better to insert nothing than to crash
64402ce3dc2d11aba2b521f85223af1f870207b81dcJean Chalard        if (null == text) text = "";
64528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mCommittedTextBeforeComposingText.append(text);
6462fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelStart += text.length() - mComposingText.length();
6472fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        mExpectedSelEnd = mExpectedSelStart;
64828d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        mComposingText.setLength(0);
649edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (isConnected()) {
65094e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge            mIC.commitCompletion(completionInfo);
65194e7f4bef970431f509a806d1b92b19fc3b5ce7dKurt Partridge        }
65228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
6535475b38328171a0841ae18074bd45380ec567e90Jean Chalard    }
65402308bec632a5df23325c916bffec5def16b22b4Jean Chalard
6552da886651874b2588f18f800417ba858ac93d88bJean Chalard    @SuppressWarnings("unused")
656d3a4c5132422b189c8dbb94dbbe84a9b9761b0a8Tadashi G. Takaoka    @Nonnull
657bb0eca57054758ef17b032d2654c1fc5f6b32101Keisuke Kuroyanagi    public NgramContext getNgramContextFromNthPreviousWord(
65817f326b7458c2bde2569e283a96e703755485328Keisuke Kuroyanagi            final SpacingAndPunctuations spacingAndPunctuations, final int n) {
659f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mIC = mParent.getCurrentInputConnection();
660edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (!isConnected()) {
661bb0eca57054758ef17b032d2654c1fc5f6b32101Keisuke Kuroyanagi            return NgramContext.EMPTY_PREV_WORDS_INFO;
66217f326b7458c2bde2569e283a96e703755485328Keisuke Kuroyanagi        }
6630232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic        final CharSequence prev = getTextBeforeCursor(NUM_CHARS_TO_GET_BEFORE_CURSOR, 0);
66428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        if (DEBUG_PREVIOUS_TEXT && null != prev) {
6650232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic            final int checkLength = NUM_CHARS_TO_GET_BEFORE_CURSOR - 1;
66628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            final String reference = prev.length() <= checkLength ? prev.toString()
66728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    : prev.subSequence(prev.length() - checkLength, prev.length()).toString();
668fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // TODO: right now the following works because mComposingText holds the part of the
669fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // composing text that is before the cursor, but this is very confusing. We should
670fe92c174ea08f9f593432f0ab20961700de9e027Jean Chalard            // fix it.
67128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            final StringBuilder internal = new StringBuilder()
67228d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    .append(mCommittedTextBeforeComposingText).append(mComposingText);
67328d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            if (internal.length() > checkLength) {
67428d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                internal.delete(0, internal.length() - checkLength);
67528d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                if (!(reference.equals(internal.toString()))) {
67628d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    final String context =
67728d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                            "Expected text = " + internal + "\nActual text = " + reference;
67828d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                    ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
67928d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard                }
68028d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard            }
68128d765ed901bfd1e736056db1cd807c13ef88c35Jean Chalard        }
682bb0eca57054758ef17b032d2654c1fc5f6b32101Keisuke Kuroyanagi        return NgramContextUtils.getNgramContextFromNthPreviousWord(
683bb843eb223ce0f8fb1088ed3393a4165123ddb1fKeisuke Kuroyanagi                prev, spacingAndPunctuations, n);
68402308bec632a5df23325c916bffec5def16b22b4Jean Chalard    }
68502308bec632a5df23325c916bffec5def16b22b4Jean Chalard
686914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard    private static boolean isPartOfCompositionForScript(final int codePoint,
687914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard            final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) {
688914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard        // We always consider word connectors part of compositions.
689914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard        return spacingAndPunctuations.isWordConnector(codePoint)
690914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard                // Otherwise, it's part of composition if it's part of script and not a separator.
691914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard                || (!spacingAndPunctuations.isWordSeparator(codePoint)
692914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard                        && ScriptUtils.isLetterPartOfScript(codePoint, scriptId));
693914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard    }
694914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard
69502308bec632a5df23325c916bffec5def16b22b4Jean Chalard    /**
69602308bec632a5df23325c916bffec5def16b22b4Jean Chalard     * Returns the text surrounding the cursor.
69702308bec632a5df23325c916bffec5def16b22b4Jean Chalard     *
698914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard     * @param spacingAndPunctuations the rules for spacing and punctuation
699292deb632cbab232334190e68d29184094d6d51bJean Chalard     * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_*
70002308bec632a5df23325c916bffec5def16b22b4Jean Chalard     * @return a range containing the text surrounding the cursor
70102308bec632a5df23325c916bffec5def16b22b4Jean Chalard     */
702914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard    public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations,
703914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard            final int scriptId) {
704f254e3fec7744dc1eb2cc09ac157986c3b2b5408Jean Chalard        mIC = mParent.getCurrentInputConnection();
705edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (!isConnected()) {
70602308bec632a5df23325c916bffec5def16b22b4Jean Chalard            return null;
70702308bec632a5df23325c916bffec5def16b22b4Jean Chalard        }
70873aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic        final CharSequence before = getTextBeforeCursorAndDetectLaggyConnection(
70973aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                OPERATION_GET_WORD_RANGE_AT_CURSOR,
710563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
71173aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                NUM_CHARS_TO_GET_BEFORE_CURSOR,
7126a114fa700d3ca73c608e1291b74bbbdd5a1a7b7Jean Chalard                InputConnection.GET_TEXT_WITH_STYLES);
713912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang        final CharSequence after = getTextAfterCursorAndDetectLaggyConnection(
71473aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                OPERATION_GET_WORD_RANGE_AT_CURSOR,
715563d7935a9e53100f6ee0ec41aff931fa28558baDan Zivkovic                SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
71673aaf6833780c1915dd4ab7d3f0f55e3af130f99Dan Zivkovic                NUM_CHARS_TO_GET_AFTER_CURSOR,
7176a114fa700d3ca73c608e1291b74bbbdd5a1a7b7Jean Chalard                InputConnection.GET_TEXT_WITH_STYLES);
71802308bec632a5df23325c916bffec5def16b22b4Jean Chalard        if (before == null || after == null) {
71902308bec632a5df23325c916bffec5def16b22b4Jean Chalard            return null;
72002308bec632a5df23325c916bffec5def16b22b4Jean Chalard        }
72102308bec632a5df23325c916bffec5def16b22b4Jean Chalard
722b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard        // Going backward, find the first breaking point (separator)
7230abc48218ee90b1d8df77dfa131ce05fbaba7121Jean Chalard        int startIndexInBefore = before.length();
724b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard        while (startIndexInBefore > 0) {
725b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard            final int codePoint = Character.codePointBefore(before, startIndexInBefore);
726914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard            if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) {
727b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard                break;
72802308bec632a5df23325c916bffec5def16b22b4Jean Chalard            }
729b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard            --startIndexInBefore;
730b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard            if (Character.isSupplementaryCodePoint(codePoint)) {
731b6695867a5fe6af999d23c669e2e6a6182457bbaJean Chalard                --startIndexInBefore;
73202308bec632a5df23325c916bffec5def16b22b4Jean Chalard            }
73302308bec632a5df23325c916bffec5def16b22b4Jean Chalard        }
73402308bec632a5df23325c916bffec5def16b22b4Jean Chalard
73502308bec632a5df23325c916bffec5def16b22b4Jean Chalard        // Find last word separator after the cursor
7360abc48218ee90b1d8df77dfa131ce05fbaba7121Jean Chalard        int endIndexInAfter = -1;
7370abc48218ee90b1d8df77dfa131ce05fbaba7121Jean Chalard        while (++endIndexInAfter < after.length()) {
7380abc48218ee90b1d8df77dfa131ce05fbaba7121Jean Chalard            final int codePoint = Character.codePointAt(after, endIndexInAfter);
739914078fd9198aeb3d7ffa034562321d688d588f7Jean Chalard            if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) {
74002308bec632a5df23325c916bffec5def16b22b4Jean Chalard                break;
74102308bec632a5df23325c916bffec5def16b22b4Jean Chalard            }
74202308bec632a5df23325c916bffec5def16b22b4Jean Chalard            if (Character.isSupplementaryCodePoint(codePoint)) {
7430abc48218ee90b1d8df77dfa131ce05fbaba7121Jean Chalard                ++endIndexInAfter;
74402308bec632a5df23325c916bffec5def16b22b4Jean Chalard            }
74502308bec632a5df23325c916bffec5def16b22b4Jean Chalard        }
74602308bec632a5df23325c916bffec5def16b22b4Jean Chalard
7477a7aeffcdcf1f25abdc3923e81fa19c2258fa9e9Jean Chalard        final boolean hasUrlSpans =
7487a7aeffcdcf1f25abdc3923e81fa19c2258fa9e9Jean Chalard                SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
7497a7aeffcdcf1f25abdc3923e81fa19c2258fa9e9Jean Chalard                || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
7503d68b066626d7e58cbe2853cd186b1ad75b90259Jean Chalard        // We don't use TextUtils#concat because it copies all spans without respect to their
7513d68b066626d7e58cbe2853cd186b1ad75b90259Jean Chalard        // nature. If the text includes a PARAGRAPH span and it has been split, then
7523d68b066626d7e58cbe2853cd186b1ad75b90259Jean Chalard        // TextUtils#concat will crash when it tries to concat both sides of it.
753f56b82f80961a511765df206eee36229cbee6ed8Ken Wakasa        return new TextRange(
754f56b82f80961a511765df206eee36229cbee6ed8Ken Wakasa                SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
7557a7aeffcdcf1f25abdc3923e81fa19c2258fa9e9Jean Chalard                        startIndexInBefore, before.length() + endIndexInAfter, before.length(),
7567a7aeffcdcf1f25abdc3923e81fa19c2258fa9e9Jean Chalard                        hasUrlSpans);
75702308bec632a5df23325c916bffec5def16b22b4Jean Chalard    }
75802308bec632a5df23325c916bffec5def16b22b4Jean Chalard
759912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang    public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
760912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang            boolean checkTextAfter) {
761912016b69f1c0e26ec58ee9d17c8ac7e5711d70dTom Ouyang        if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
76272b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard            // If what's after the cursor is a word character, then we're touching a word.
76372b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard            return true;
76472b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        }
76572b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        final String textBeforeCursor = mCommittedTextBeforeComposingText.toString();
76672b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        int indexOfCodePointInJavaChars = textBeforeCursor.length();
76772b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
76872b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard                : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
76972b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        // Search for the first non word-connector char
77072b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) {
77172b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard            indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint);
77272b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard            consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
77372b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard                    : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
77472b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        }
77572b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard        return !(Constants.NOT_A_CODE == consideredCodePoint
77672b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard                || spacingAndPunctuations.isWordSeparator(consideredCodePoint)
77772b67f65411cf07cb8cb2d52e859f46d9d5b91d4Jean Chalard                || spacingAndPunctuations.isWordConnector(consideredCodePoint));
778e5cdcaff658f5366621115a728cb683eab5fda0aJean Chalard    }
779e5cdcaff658f5366621115a728cb683eab5fda0aJean Chalard
7806a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka    public boolean isCursorFollowedByWordCharacter(
7816a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka            final SpacingAndPunctuations spacingAndPunctuations) {
7820a064a5d88c5cdc03f8ef13a2f7b6b506b9cbdfaJean Chalard        final CharSequence after = getTextAfterCursor(1, 0);
7836a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        if (TextUtils.isEmpty(after)) {
7846a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka            return false;
7856a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        }
7866a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        final int codePointAfterCursor = Character.codePointAt(after, 0);
7876a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor)
7886a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka                || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) {
7896a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka            return false;
790bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard        }
7916a18af634eda872daad858acbddae2a15452952eTadashi G. Takaoka        return true;
792bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard    }
793bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard
794bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard    public void removeTrailingSpace() {
795574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
7960a064a5d88c5cdc03f8ef13a2f7b6b506b9cbdfaJean Chalard        final int codePointBeforeCursor = getCodePointBeforeCursor();
7970a064a5d88c5cdc03f8ef13a2f7b6b506b9cbdfaJean Chalard        if (Constants.CODE_SPACE == codePointBeforeCursor) {
7980232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic            deleteTextBeforeCursor(1);
799bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard        }
800bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard    }
801bbbdab12be748cdc2158f0e04bbb5478052ecd89Jean Chalard
802747cf0435a7e978dfd43c30bd931b56146c3d852Jean Chalard    public boolean sameAsTextBeforeCursor(final CharSequence text) {
803747cf0435a7e978dfd43c30bd931b56146c3d852Jean Chalard        final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
804747cf0435a7e978dfd43c30bd931b56146c3d852Jean Chalard        return TextUtils.equals(text, beforeText);
805747cf0435a7e978dfd43c30bd931b56146c3d852Jean Chalard    }
806747cf0435a7e978dfd43c30bd931b56146c3d852Jean Chalard
807b41ee671944aee97163c1567d7407fc3bd7507a3Jean Chalard    public boolean revertDoubleSpacePeriod(final SpacingAndPunctuations spacingAndPunctuations) {
808574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
809a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard        // Here we test whether we indeed have a period and a space before us. This should not
810a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard        // be needed, but it's there just in case something went wrong.
811a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard        final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
812b41ee671944aee97163c1567d7407fc3bd7507a3Jean Chalard        if (!TextUtils.equals(spacingAndPunctuations.mSentenceSeparatorAndSpace,
813b41ee671944aee97163c1567d7407fc3bd7507a3Jean Chalard                textBeforeCursor)) {
814a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard            // Theoretically we should not be coming here if there isn't ". " before the
815a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard            // cursor, but the application may be changing the text while we are typing, so
816a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard            // anything goes. We should not crash.
817b41ee671944aee97163c1567d7407fc3bd7507a3Jean Chalard            Log.d(TAG, "Tried to revert double-space combo but we didn't find \""
818b41ee671944aee97163c1567d7407fc3bd7507a3Jean Chalard                    + spacingAndPunctuations.mSentenceSeparatorAndSpace
819b41ee671944aee97163c1567d7407fc3bd7507a3Jean Chalard                    + "\" just before the cursor.");
820a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard            return false;
821a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard        }
822522d13c302e360cd4984b9164a9fc81bdc64feacJean Chalard        // Double-space results in ". ". A backspace to cancel this should result in a single
823522d13c302e360cd4984b9164a9fc81bdc64feacJean Chalard        // space in the text field, so we replace ". " with a single space.
8240232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic        deleteTextBeforeCursor(2);
825522d13c302e360cd4984b9164a9fc81bdc64feacJean Chalard        final String singleSpace = " ";
826522d13c302e360cd4984b9164a9fc81bdc64feacJean Chalard        commitText(singleSpace, 1);
827a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard        return true;
828a32eb2721390d5964c83c787ad30fd3f61b936b0Jean Chalard    }
8292010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard
8302010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard    public boolean revertSwapPunctuation() {
831574b80aacee95df26e85e6b78876a73d7076a672Jean Chalard        if (DEBUG_BATCH_NESTING) checkBatchEdit();
8322010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        // Here we test whether we indeed have a space and something else before us. This should not
8332010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        // be needed, but it's there just in case something went wrong.
8342010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
8352010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
8362010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        // enter surrogate pairs this code will have been removed.
8372010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        if (TextUtils.isEmpty(textBeforeCursor)
838240871ecafde7834ebb4270cd7758fc904a5f3a7Tadashi G. Takaoka                || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) {
8392010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard            // We may only come here if the application is changing the text while we are typing.
8402010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
8412010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard            // but some debugging log may be in order.
8422010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
8432010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard                    + "find a space just before the cursor.");
8442010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard            return false;
8452010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        }
8460232e73dfa5e7cadf3f0698407fe6aecd97f3227Dan Zivkovic        deleteTextBeforeCursor(2);
847f763dc5915d394378bdcdc90cc0b238e66926b8bKurt Partridge        final String text = " " + textBeforeCursor.subSequence(0, 1);
848f763dc5915d394378bdcdc90cc0b238e66926b8bKurt Partridge        commitText(text, 1);
8492010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard        return true;
8502010aad741bc1a7266913bcb8b8348d6e401c95bJean Chalard    }
8515ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard
8525ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard    /**
8535ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * Heuristic to determine if this is an expected update of the cursor.
8545ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     *
8555ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * Sometimes updates to the cursor position are late because of their asynchronous nature.
8565ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * This method tries to determine if this update is one, based on the values of the cursor
8575ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * position in the update, and the currently expected position of the cursor according to
8585ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * LatinIME's internal accounting. If this is not a belated expected update, then it should
8595ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * mean that the user moved the cursor explicitly.
8605ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * This is quite robust, but of course it's not perfect. In particular, it will fail in the
8615ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * case we get an update A, the user types in N characters so as to move the cursor to A+N but
8625ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * we don't get those, and then the user places the cursor between A and A+N, and we get only
8635ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * this update and not the ones in-between. This is almost impossible to achieve even trying
8645ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * very very hard.
8655ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     *
8662fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param oldSelStart The value of the old selection in the update.
8672fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param newSelStart The value of the new selection in the update.
8682fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param oldSelEnd The value of the old selection end in the update.
8692fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa     * @param newSelEnd The value of the new selection end in the update.
8705ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     * @return whether this is a belated expected update or not.
8715ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard     */
8722fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa    public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart,
8732fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa            final int oldSelEnd, final int newSelEnd) {
874ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        // This update is "belated" if we are expecting it. That is, mExpectedSelStart and
8752fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        // mExpectedSelEnd match the new values that the TextView is updating TO.
8762fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true;
877ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old
878ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        // values, and one of newSelStart or newSelEnd is updated to a different value. In this
879be8ad77ebb5d78db18a2ca1992e5e73d2b5e158aJean Chalard        // case, it is likely that something other than the IME has moved the selection endpoint
8802fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        // to the new value.
8812fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd
8822fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false;
883be8ad77ebb5d78db18a2ca1992e5e73d2b5e158aJean Chalard        // If neither of the above two cases hold, then the system may be having trouble keeping up
884ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart
8852fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then
8862fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        // assume a belated update.
8872fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa        return (newSelStart == newSelEnd)
8882fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0
8892fa3693c264a4c150ac307d9bb7f6f8f18cc4ffcKen Wakasa                && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0;
8905ed88457bf9ef3305d4a5aa4ac05b513433ad0ddJean Chalard    }
89173ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard
89273ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard    /**
89373ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard     * Looks at the text just before the cursor to find out if it looks like a URL.
89473ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard     *
89573ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard     * The weakest point here is, if we don't have enough text bufferized, we may fail to realize
89673ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard     * we are in URL situation, but other places in this class have the same limitation and it
89773ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard     * does not matter too much in the practice.
89873ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard     */
89973ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard    public boolean textBeforeCursorLooksLikeURL() {
90073ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard        return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
90173ec85b8ad3102ce1c7e6013be73afe83475e589Jean Chalard    }
902ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard
903ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    /**
904c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard     * Looks at the text just before the cursor to find out if we are inside a double quote.
905c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard     *
906c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard     * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached.
907c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard     * However this won't be a concrete problem in most situations, as the cache is almost always
908c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard     * long enough for this use.
909c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard     */
910c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard    public boolean isInsideDoubleQuoteOrAfterDigit() {
911c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard        return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText);
912c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard    }
913c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard
914c7ef305bbc119b820fd619d3ed205198d4f98c3fJean Chalard    /**
915ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     * Try to get the text from the editor to expose lies the framework may have been
91638144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard     * telling us. Concretely, when the device rotates and when the keyboard reopens in the same
91738144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard     * text field after having been closed with the back key, the frameworks tells us about where
91838144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard     * the cursor used to be initially in the editor at the time it first received the focus; this
919ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     * may be completely different from the place it is upon rotation. Since we don't have any
920ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     * means to get the real value, try at least to ask the text view for some characters and
921ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     * detect the most damaging cases: when the cursor position is declared to be much smaller
922ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     * than it really is.
923ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     */
924ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    public void tryFixLyingCursorPosition() {
9257d74594123bd9ee3a09f54d509bff27af5f6cca8Jean Chalard        mIC = mParent.getCurrentInputConnection();
926ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        final CharSequence textBeforeCursor = getTextBeforeCursor(
927ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
928edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        final CharSequence selectedText = isConnected() ? mIC.getSelectedText(0 /* flags */) : null;
92938144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard        if (null == textBeforeCursor ||
93038144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard                (!TextUtils.isEmpty(selectedText) && mExpectedSelEnd == mExpectedSelStart)) {
93138144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // If textBeforeCursor is null, we have no idea what kind of text field we have or if
93238144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // thinking about the "cursor position" actually makes any sense. In this case we
93338144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // remember a meaningless cursor position. Contrast this with an empty string, which is
93438144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // valid and should mean the cursor is at the start of the text.
93538144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // Also, if we expect we don't have a selection but we DO have non-empty selected text,
93638144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // then the framework lied to us about the cursor position. In this case, we should just
93738144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // revert to the most basic behavior possible for the next action (backspace in
93838144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // particular comes to mind), so we remember a meaningless cursor position which should
93938144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // result in degraded behavior from the next input.
94038144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // Interestingly, in either case, chances are any action the user takes next will result
94138144047ea3985c9345e7fbe6bb3aafbeaea5f06Jean Chalard            // in a call to onUpdateSelection, which should set things right.
942ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard            mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION;
943ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        } else {
944ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard            final int textLength = textBeforeCursor.length();
9453b4c1d30565c06a7f5a4c362002862e8bcb61f01Jean Chalard            if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
9463b4c1d30565c06a7f5a4c362002862e8bcb61f01Jean Chalard                    && (textLength > mExpectedSelStart
9473b4c1d30565c06a7f5a4c362002862e8bcb61f01Jean Chalard                            ||  mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
948ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // It should not be possible to have only one of those variables be
949ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
950ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // (simple cursor, no selection) or there is no cursor/we don't know its pos
951ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd;
952ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                mExpectedSelStart = textLength;
953ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // We can't figure out the value of mLastSelectionEnd :(
954ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // But at least if it's smaller than mLastSelectionStart something is wrong,
955ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // and if they used to be equal we also don't want to make it look like there is a
956ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                // selection.
957ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                if (wasEqual || mExpectedSelStart > mExpectedSelEnd) {
958ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                    mExpectedSelEnd = mExpectedSelStart;
959ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard                }
960ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard            }
961ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        }
962ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    }
963ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard
964c92c883fdf2287b49392692fa2e8d109dc26f785David Faden    @Override
965c92c883fdf2287b49392692fa2e8d109dc26f785David Faden    public boolean performPrivateCommand(final String action, final Bundle data) {
966c92c883fdf2287b49392692fa2e8d109dc26f785David Faden        mIC = mParent.getCurrentInputConnection();
967edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (!isConnected()) {
968c92c883fdf2287b49392692fa2e8d109dc26f785David Faden            return false;
969c92c883fdf2287b49392692fa2e8d109dc26f785David Faden        }
970c92c883fdf2287b49392692fa2e8d109dc26f785David Faden        return mIC.performPrivateCommand(action, data);
971c92c883fdf2287b49392692fa2e8d109dc26f785David Faden    }
972c92c883fdf2287b49392692fa2e8d109dc26f785David Faden
973ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    public int getExpectedSelectionStart() {
974ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        return mExpectedSelStart;
975ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    }
976ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard
977ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    public int getExpectedSelectionEnd() {
978ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        return mExpectedSelEnd;
979ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    }
980ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard
981ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    /**
982ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     * @return whether there is a selection currently active.
983ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard     */
984ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    public boolean hasSelection() {
985ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard        return mExpectedSelEnd != mExpectedSelStart;
986ecf46df22683b997311ba77b34b33dffa8be43c0Jean Chalard    }
9879fd9a68d8797ed500d07d5e149cd4da50be2df15Jean Chalard
9889fd9a68d8797ed500d07d5e149cd4da50be2df15Jean Chalard    public boolean isCursorPositionKnown() {
9899fd9a68d8797ed500d07d5e149cd4da50be2df15Jean Chalard        return INVALID_CURSOR_POSITION != mExpectedSelStart;
9909fd9a68d8797ed500d07d5e149cd4da50be2df15Jean Chalard    }
9919273f3832b51f5d23d86df624600381ed6d6585fJean Chalard
9929273f3832b51f5d23d86df624600381ed6d6585fJean Chalard    /**
9939273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     * Work around a bug that was present before Jelly Bean upon rotation.
9949273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     *
9959273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     * Before Jelly Bean, there is a bug where setComposingRegion and other committing
9969273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     * functions on the input connection get ignored until the cursor moves. This method works
9979273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     * around the bug by wiggling the cursor first, which reactivates the connection and has
9989273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     * the subsequent methods work, then restoring it to its original position.
9999273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     *
10009273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     * On platforms on which this method is not present, this is a no-op.
10019273f3832b51f5d23d86df624600381ed6d6585fJean Chalard     */
10029273f3832b51f5d23d86df624600381ed6d6585fJean Chalard    public void maybeMoveTheCursorAroundAndRestoreToWorkaroundABug() {
10035254c01d4cc024527479d4dc5fab2ed2516c395cDan Zivkovic        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
10049273f3832b51f5d23d86df624600381ed6d6585fJean Chalard            if (mExpectedSelStart > 0) {
10059273f3832b51f5d23d86df624600381ed6d6585fJean Chalard                mIC.setSelection(mExpectedSelStart - 1, mExpectedSelStart - 1);
10069273f3832b51f5d23d86df624600381ed6d6585fJean Chalard            } else {
10079273f3832b51f5d23d86df624600381ed6d6585fJean Chalard                mIC.setSelection(mExpectedSelStart + 1, mExpectedSelStart + 1);
10089273f3832b51f5d23d86df624600381ed6d6585fJean Chalard            }
10099273f3832b51f5d23d86df624600381ed6d6585fJean Chalard            mIC.setSelection(mExpectedSelStart, mExpectedSelEnd);
10109273f3832b51f5d23d86df624600381ed6d6585fJean Chalard        }
10119273f3832b51f5d23d86df624600381ed6d6585fJean Chalard    }
101229200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa
101329200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa    /**
101429200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}.
101529200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * @param enableMonitor {@code true} to request the editor to call back the method whenever the
101629200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * cursor/anchor position is changed.
101729200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * @param requestImmediateCallback {@code true} to request the editor to call back the method
101829200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * as soon as possible to notify the current cursor/anchor position to the input method.
101929200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * @return {@code true} if the request is accepted. Returns {@code false} otherwise, which
102029200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * includes "not implemented" or "rejected" or "temporarily unavailable" or whatever which
102129200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * prevents the application from fulfilling the request. (TODO: Improve the API when it turns
102229200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     * out that we actually need more detailed error codes)
102329200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa     */
10245d6ac77732b6fe29489deecc297d771642150a2bYohei Yukawa    public boolean requestCursorUpdates(final boolean enableMonitor,
102529200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa            final boolean requestImmediateCallback) {
1026acce1aa59eac6816fe3ce1fcb014666fc71a40f1Yohei Yukawa        mIC = mParent.getCurrentInputConnection();
1027edd94a449e293c07779ac50e98aca3ad92910d92Dan Zivkovic        if (!isConnected()) {
1028a6d2cf5ec71fdba86455ff8281a121f32a3248e3Dan Zivkovic            return false;
1029acce1aa59eac6816fe3ce1fcb014666fc71a40f1Yohei Yukawa        }
1030a6d2cf5ec71fdba86455ff8281a121f32a3248e3Dan Zivkovic        return InputConnectionCompatUtils.requestCursorUpdates(
1031a6d2cf5ec71fdba86455ff8281a121f32a3248e3Dan Zivkovic                mIC, enableMonitor, requestImmediateCallback);
103229200b0abe1d65aa2f9ddefd247ab91563d666f8Yohei Yukawa    }
10335475b38328171a0841ae18074bd45380ec567e90Jean Chalard}
1034