Home | History | Annotate | Download | only in input
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.content.browser.input;
      6 
      7 import android.os.SystemClock;
      8 import android.text.Editable;
      9 import android.text.InputType;
     10 import android.text.Selection;
     11 import android.text.TextUtils;
     12 import android.util.Log;
     13 import android.view.KeyEvent;
     14 import android.view.View;
     15 import android.view.inputmethod.BaseInputConnection;
     16 import android.view.inputmethod.EditorInfo;
     17 import android.view.inputmethod.ExtractedText;
     18 import android.view.inputmethod.ExtractedTextRequest;
     19 
     20 import org.chromium.base.VisibleForTesting;
     21 
     22 /**
     23  * InputConnection is created by ContentView.onCreateInputConnection.
     24  * It then adapts android's IME to chrome's RenderWidgetHostView using the
     25  * native ImeAdapterAndroid via the class ImeAdapter.
     26  */
     27 public class AdapterInputConnection extends BaseInputConnection {
     28     private static final String TAG = "AdapterInputConnection";
     29     private static final boolean DEBUG = false;
     30     /**
     31      * Selection value should be -1 if not known. See EditorInfo.java for details.
     32      */
     33     public static final int INVALID_SELECTION = -1;
     34     public static final int INVALID_COMPOSITION = -1;
     35 
     36     private final View mInternalView;
     37     private final ImeAdapter mImeAdapter;
     38     private final Editable mEditable;
     39 
     40     private boolean mSingleLine;
     41     private int mNumNestedBatchEdits = 0;
     42 
     43     private int mLastUpdateSelectionStart = INVALID_SELECTION;
     44     private int mLastUpdateSelectionEnd = INVALID_SELECTION;
     45     private int mLastUpdateCompositionStart = INVALID_COMPOSITION;
     46     private int mLastUpdateCompositionEnd = INVALID_COMPOSITION;
     47 
     48     @VisibleForTesting
     49     AdapterInputConnection(View view, ImeAdapter imeAdapter, Editable editable,
     50             EditorInfo outAttrs) {
     51         super(view, true);
     52         mInternalView = view;
     53         mImeAdapter = imeAdapter;
     54         mImeAdapter.setInputConnection(this);
     55         mEditable = editable;
     56         // The editable passed in might have been in use by a prior keyboard and could have had
     57         // prior composition spans set.  To avoid keyboard conflicts, remove all composing spans
     58         // when taking ownership of an existing Editable.
     59         removeComposingSpans(mEditable);
     60         mSingleLine = true;
     61         outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
     62                 | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
     63         outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
     64                 | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
     65 
     66         int inputType = imeAdapter.getTextInputType();
     67         int inputFlags = imeAdapter.getTextInputFlags();
     68         if ((inputFlags & imeAdapter.sTextInputFlagAutocompleteOff) != 0) {
     69             outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
     70         }
     71 
     72         if (inputType == ImeAdapter.sTextInputTypeText) {
     73             // Normal text field
     74             outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
     75             if ((inputFlags & imeAdapter.sTextInputFlagAutocorrectOff) == 0) {
     76                 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
     77             }
     78         } else if (inputType == ImeAdapter.sTextInputTypeTextArea ||
     79                 inputType == ImeAdapter.sTextInputTypeContentEditable) {
     80             // TextArea or contenteditable.
     81             outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
     82                     | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
     83             if ((inputFlags & imeAdapter.sTextInputFlagAutocorrectOff) == 0) {
     84                 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
     85             }
     86             outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
     87             mSingleLine = false;
     88         } else if (inputType == ImeAdapter.sTextInputTypePassword) {
     89             // Password
     90             outAttrs.inputType = InputType.TYPE_CLASS_TEXT
     91                     | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
     92             outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
     93         } else if (inputType == ImeAdapter.sTextInputTypeSearch) {
     94             // Search
     95             outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
     96         } else if (inputType == ImeAdapter.sTextInputTypeUrl) {
     97             // Url
     98             outAttrs.inputType = InputType.TYPE_CLASS_TEXT
     99                     | InputType.TYPE_TEXT_VARIATION_URI;
    100             outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
    101         } else if (inputType == ImeAdapter.sTextInputTypeEmail) {
    102             // Email
    103             outAttrs.inputType = InputType.TYPE_CLASS_TEXT
    104                     | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
    105             outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
    106         } else if (inputType == ImeAdapter.sTextInputTypeTel) {
    107             // Telephone
    108             // Number and telephone do not have both a Tab key and an
    109             // action in default OSK, so set the action to NEXT
    110             outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
    111             outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
    112         } else if (inputType == ImeAdapter.sTextInputTypeNumber) {
    113             // Number
    114             outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
    115                     | InputType.TYPE_NUMBER_VARIATION_NORMAL
    116                     | InputType.TYPE_NUMBER_FLAG_DECIMAL;
    117             outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
    118         }
    119         outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
    120         outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
    121         mLastUpdateSelectionStart = Selection.getSelectionStart(mEditable);
    122         mLastUpdateSelectionEnd = Selection.getSelectionEnd(mEditable);
    123 
    124         Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd);
    125         updateSelectionIfRequired();
    126     }
    127 
    128     /**
    129      * Updates the AdapterInputConnection's internal representation of the text being edited and
    130      * its selection and composition properties. The resulting Editable is accessible through the
    131      * getEditable() method. If the text has not changed, this also calls updateSelection on the
    132      * InputMethodManager.
    133      *
    134      * @param text The String contents of the field being edited.
    135      * @param selectionStart The character offset of the selection start, or the caret position if
    136      *                       there is no selection.
    137      * @param selectionEnd The character offset of the selection end, or the caret position if there
    138      *                     is no selection.
    139      * @param compositionStart The character offset of the composition start, or -1 if there is no
    140      *                         composition.
    141      * @param compositionEnd The character offset of the composition end, or -1 if there is no
    142      *                       selection.
    143      * @param isNonImeChange True when the update was caused by non-IME (e.g. Javascript).
    144      */
    145     @VisibleForTesting
    146     public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart,
    147             int compositionEnd, boolean isNonImeChange) {
    148         if (DEBUG) {
    149             Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] ["
    150                     + compositionStart + " " + compositionEnd + "] [" + isNonImeChange + "]");
    151         }
    152         // If this update is from the IME, no further state modification is necessary because the
    153         // state should have been updated already by the IM framework directly.
    154         if (!isNonImeChange) return;
    155 
    156         // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
    157         text = text.replace('\u00A0', ' ');
    158 
    159         selectionStart = Math.min(selectionStart, text.length());
    160         selectionEnd = Math.min(selectionEnd, text.length());
    161         compositionStart = Math.min(compositionStart, text.length());
    162         compositionEnd = Math.min(compositionEnd, text.length());
    163 
    164         String prevText = mEditable.toString();
    165         boolean textUnchanged = prevText.equals(text);
    166 
    167         if (!textUnchanged) {
    168             mEditable.replace(0, mEditable.length(), text);
    169         }
    170 
    171         Selection.setSelection(mEditable, selectionStart, selectionEnd);
    172 
    173         if (compositionStart == compositionEnd) {
    174             removeComposingSpans(mEditable);
    175         } else {
    176             super.setComposingRegion(compositionStart, compositionEnd);
    177         }
    178         updateSelectionIfRequired();
    179     }
    180 
    181     /**
    182      * @return Editable object which contains the state of current focused editable element.
    183      */
    184     @Override
    185     public Editable getEditable() {
    186         return mEditable;
    187     }
    188 
    189     /**
    190      * Sends selection update to the InputMethodManager unless we are currently in a batch edit or
    191      * if the exact same selection and composition update was sent already.
    192      */
    193     private void updateSelectionIfRequired() {
    194         if (mNumNestedBatchEdits != 0) return;
    195         int selectionStart = Selection.getSelectionStart(mEditable);
    196         int selectionEnd = Selection.getSelectionEnd(mEditable);
    197         int compositionStart = getComposingSpanStart(mEditable);
    198         int compositionEnd = getComposingSpanEnd(mEditable);
    199         // Avoid sending update if we sent an exact update already previously.
    200         if (mLastUpdateSelectionStart == selectionStart &&
    201                 mLastUpdateSelectionEnd == selectionEnd &&
    202                 mLastUpdateCompositionStart == compositionStart &&
    203                 mLastUpdateCompositionEnd == compositionEnd) {
    204             return;
    205         }
    206         if (DEBUG) {
    207             Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] ["
    208                     + compositionStart + " " + compositionEnd + "]");
    209         }
    210         // updateSelection should be called every time the selection or composition changes
    211         // if it happens not within a batch edit, or at the end of each top level batch edit.
    212         getInputMethodManagerWrapper().updateSelection(mInternalView,
    213                 selectionStart, selectionEnd, compositionStart, compositionEnd);
    214         mLastUpdateSelectionStart = selectionStart;
    215         mLastUpdateSelectionEnd = selectionEnd;
    216         mLastUpdateCompositionStart = compositionStart;
    217         mLastUpdateCompositionEnd = compositionEnd;
    218     }
    219 
    220     /**
    221      * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
    222      */
    223     @Override
    224     public boolean setComposingText(CharSequence text, int newCursorPosition) {
    225         if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]");
    226         if (maybePerformEmptyCompositionWorkaround(text)) return true;
    227         super.setComposingText(text, newCursorPosition);
    228         updateSelectionIfRequired();
    229         return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition, false);
    230     }
    231 
    232     /**
    233      * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
    234      */
    235     @Override
    236     public boolean commitText(CharSequence text, int newCursorPosition) {
    237         if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
    238         if (maybePerformEmptyCompositionWorkaround(text)) return true;
    239         super.commitText(text, newCursorPosition);
    240         updateSelectionIfRequired();
    241         return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition,
    242                 text.length() > 0);
    243     }
    244 
    245     /**
    246      * @see BaseInputConnection#performEditorAction(int)
    247      */
    248     @Override
    249     public boolean performEditorAction(int actionCode) {
    250         if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
    251         if (actionCode == EditorInfo.IME_ACTION_NEXT) {
    252             restartInput();
    253             // Send TAB key event
    254             long timeStampMs = SystemClock.uptimeMillis();
    255             mImeAdapter.sendSyntheticKeyEvent(
    256                     ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0, 0);
    257         } else {
    258             mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
    259                     KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
    260                     | KeyEvent.FLAG_EDITOR_ACTION);
    261         }
    262         return true;
    263     }
    264 
    265     /**
    266      * @see BaseInputConnection#performContextMenuAction(int)
    267      */
    268     @Override
    269     public boolean performContextMenuAction(int id) {
    270         if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
    271         switch (id) {
    272             case android.R.id.selectAll:
    273                 return mImeAdapter.selectAll();
    274             case android.R.id.cut:
    275                 return mImeAdapter.cut();
    276             case android.R.id.copy:
    277                 return mImeAdapter.copy();
    278             case android.R.id.paste:
    279                 return mImeAdapter.paste();
    280             default:
    281                 return false;
    282         }
    283     }
    284 
    285     /**
    286      * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
    287      *                                           int)
    288      */
    289     @Override
    290     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
    291         if (DEBUG) Log.w(TAG, "getExtractedText");
    292         ExtractedText et = new ExtractedText();
    293         et.text = mEditable.toString();
    294         et.partialEndOffset = mEditable.length();
    295         et.selectionStart = Selection.getSelectionStart(mEditable);
    296         et.selectionEnd = Selection.getSelectionEnd(mEditable);
    297         et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
    298         return et;
    299     }
    300 
    301     /**
    302      * @see BaseInputConnection#beginBatchEdit()
    303      */
    304     @Override
    305     public boolean beginBatchEdit() {
    306         if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
    307         mNumNestedBatchEdits++;
    308         return true;
    309     }
    310 
    311     /**
    312      * @see BaseInputConnection#endBatchEdit()
    313      */
    314     @Override
    315     public boolean endBatchEdit() {
    316         if (mNumNestedBatchEdits == 0) return false;
    317         --mNumNestedBatchEdits;
    318         if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
    319         if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
    320         return mNumNestedBatchEdits != 0;
    321     }
    322 
    323     /**
    324      * @see BaseInputConnection#deleteSurroundingText(int, int)
    325      */
    326     @Override
    327     public boolean deleteSurroundingText(int beforeLength, int afterLength) {
    328         if (DEBUG) {
    329             Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
    330         }
    331         int originalBeforeLength = beforeLength;
    332         int originalAfterLength = afterLength;
    333         int availableBefore = Selection.getSelectionStart(mEditable);
    334         int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable);
    335         beforeLength = Math.min(beforeLength, availableBefore);
    336         afterLength = Math.min(afterLength, availableAfter);
    337         super.deleteSurroundingText(beforeLength, afterLength);
    338         updateSelectionIfRequired();
    339 
    340         // For single-char deletion calls |ImeAdapter.sendKeyEventWithKeyCode| with the real key
    341         // code. For multi-character deletion, executes deletion by calling
    342         // |ImeAdapter.deleteSurroundingText| and sends synthetic key events with a dummy key code.
    343         int keyCode = KeyEvent.KEYCODE_UNKNOWN;
    344         if (originalBeforeLength == 1 && originalAfterLength == 0)
    345             keyCode = KeyEvent.KEYCODE_DEL;
    346         else if (originalBeforeLength == 0 && originalAfterLength == 1)
    347             keyCode = KeyEvent.KEYCODE_FORWARD_DEL;
    348 
    349         boolean result = true;
    350         if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
    351             result = mImeAdapter.sendSyntheticKeyEvent(
    352                     ImeAdapter.sEventTypeRawKeyDown, SystemClock.uptimeMillis(), keyCode, 0, 0);
    353             result &= mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
    354             result &= mImeAdapter.sendSyntheticKeyEvent(
    355                     ImeAdapter.sEventTypeKeyUp, SystemClock.uptimeMillis(), keyCode, 0, 0);
    356         } else {
    357             mImeAdapter.sendKeyEventWithKeyCode(
    358                     keyCode, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
    359         }
    360         return result;
    361     }
    362 
    363     /**
    364      * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
    365      */
    366     @Override
    367     public boolean sendKeyEvent(KeyEvent event) {
    368         if (DEBUG) {
    369             Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
    370         }
    371         // If this is a key-up, and backspace/del or if the key has a character representation,
    372         // need to update the underlying Editable (i.e. the local representation of the text
    373         // being edited).
    374         if (event.getAction() == KeyEvent.ACTION_UP) {
    375             if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
    376                 deleteSurroundingText(1, 0);
    377                 return true;
    378             } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
    379                 deleteSurroundingText(0, 1);
    380                 return true;
    381             } else {
    382                 int unicodeChar = event.getUnicodeChar();
    383                 if (unicodeChar != 0) {
    384                     int selectionStart = Selection.getSelectionStart(mEditable);
    385                     int selectionEnd = Selection.getSelectionEnd(mEditable);
    386                     if (selectionStart > selectionEnd) {
    387                         int temp = selectionStart;
    388                         selectionStart = selectionEnd;
    389                         selectionEnd = temp;
    390                     }
    391                     mEditable.replace(selectionStart, selectionEnd,
    392                             Character.toString((char) unicodeChar));
    393                 }
    394             }
    395         } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
    396             // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
    397             if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
    398                 beginBatchEdit();
    399                 finishComposingText();
    400                 mImeAdapter.translateAndSendNativeEvents(event);
    401                 endBatchEdit();
    402                 return true;
    403             } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
    404                 return true;
    405             } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
    406                 return true;
    407             }
    408         }
    409         mImeAdapter.translateAndSendNativeEvents(event);
    410         return true;
    411     }
    412 
    413     /**
    414      * @see BaseInputConnection#finishComposingText()
    415      */
    416     @Override
    417     public boolean finishComposingText() {
    418         if (DEBUG) Log.w(TAG, "finishComposingText");
    419         if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
    420             return true;
    421         }
    422 
    423         super.finishComposingText();
    424         updateSelectionIfRequired();
    425         mImeAdapter.finishComposingText();
    426 
    427         return true;
    428     }
    429 
    430     /**
    431      * @see BaseInputConnection#setSelection(int, int)
    432      */
    433     @Override
    434     public boolean setSelection(int start, int end) {
    435         if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
    436         int textLength = mEditable.length();
    437         if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
    438         super.setSelection(start, end);
    439         updateSelectionIfRequired();
    440         return mImeAdapter.setEditableSelectionOffsets(start, end);
    441     }
    442 
    443     /**
    444      * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
    445      * state is no longer what the IME has and that it needs to be updated.
    446      */
    447     void restartInput() {
    448         if (DEBUG) Log.w(TAG, "restartInput");
    449         getInputMethodManagerWrapper().restartInput(mInternalView);
    450         mNumNestedBatchEdits = 0;
    451     }
    452 
    453     /**
    454      * @see BaseInputConnection#setComposingRegion(int, int)
    455      */
    456     @Override
    457     public boolean setComposingRegion(int start, int end) {
    458         if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
    459         int textLength = mEditable.length();
    460         int a = Math.min(start, end);
    461         int b = Math.max(start, end);
    462         if (a < 0) a = 0;
    463         if (b < 0) b = 0;
    464         if (a > textLength) a = textLength;
    465         if (b > textLength) b = textLength;
    466 
    467         if (a == b) {
    468             removeComposingSpans(mEditable);
    469         } else {
    470             super.setComposingRegion(a, b);
    471         }
    472         updateSelectionIfRequired();
    473 
    474         CharSequence regionText = null;
    475         if (b > a) {
    476             regionText = mEditable.subSequence(a, b);
    477         }
    478         return mImeAdapter.setComposingRegion(regionText, a, b);
    479     }
    480 
    481     boolean isActive() {
    482         return getInputMethodManagerWrapper().isActive(mInternalView);
    483     }
    484 
    485     private InputMethodManagerWrapper getInputMethodManagerWrapper() {
    486         return mImeAdapter.getInputMethodManagerWrapper();
    487     }
    488 
    489     /**
    490      * This method works around the issue crbug.com/373934 where Blink does not cancel
    491      * the composition when we send a commit with the empty text.
    492      *
    493      * TODO(aurimas) Remove this once crbug.com/373934 is fixed.
    494      *
    495      * @param text Text that software keyboard requested to commit.
    496      * @return Whether the workaround was performed.
    497      */
    498     private boolean maybePerformEmptyCompositionWorkaround(CharSequence text) {
    499         int selectionStart = Selection.getSelectionStart(mEditable);
    500         int selectionEnd = Selection.getSelectionEnd(mEditable);
    501         int compositionStart = getComposingSpanStart(mEditable);
    502         int compositionEnd = getComposingSpanEnd(mEditable);
    503         if (TextUtils.isEmpty(text) && (selectionStart == selectionEnd)
    504                 && compositionStart != INVALID_COMPOSITION
    505                 && compositionEnd != INVALID_COMPOSITION) {
    506             beginBatchEdit();
    507             finishComposingText();
    508             int selection = Selection.getSelectionStart(mEditable);
    509             deleteSurroundingText(selection - compositionStart, selection - compositionEnd);
    510             endBatchEdit();
    511             return true;
    512         }
    513         return false;
    514     }
    515 
    516     @VisibleForTesting
    517     static class ImeState {
    518         public final String text;
    519         public final int selectionStart;
    520         public final int selectionEnd;
    521         public final int compositionStart;
    522         public final int compositionEnd;
    523 
    524         public ImeState(String text, int selectionStart, int selectionEnd,
    525                 int compositionStart, int compositionEnd) {
    526             this.text = text;
    527             this.selectionStart = selectionStart;
    528             this.selectionEnd = selectionEnd;
    529             this.compositionStart = compositionStart;
    530             this.compositionEnd = compositionEnd;
    531         }
    532     }
    533 
    534     @VisibleForTesting
    535     ImeState getImeStateForTesting() {
    536         String text = mEditable.toString();
    537         int selectionStart = Selection.getSelectionStart(mEditable);
    538         int selectionEnd = Selection.getSelectionEnd(mEditable);
    539         int compositionStart = getComposingSpanStart(mEditable);
    540         int compositionEnd = getComposingSpanEnd(mEditable);
    541         return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);
    542     }
    543 }
    544