RichInputConnection.java revision acce1aa59eac6816fe3ce1fcb014666fc71a40f1
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.graphics.Color; 20import android.inputmethodservice.InputMethodService; 21import android.os.Build; 22import android.text.SpannableStringBuilder; 23import android.text.Spanned; 24import android.text.TextUtils; 25import android.text.style.BackgroundColorSpan; 26import android.util.Log; 27import android.view.KeyEvent; 28import android.view.inputmethod.CompletionInfo; 29import android.view.inputmethod.CorrectionInfo; 30import android.view.inputmethod.ExtractedText; 31import android.view.inputmethod.ExtractedTextRequest; 32import android.view.inputmethod.InputConnection; 33import android.view.inputmethod.InputMethodManager; 34 35import com.android.inputmethod.compat.InputConnectionCompatUtils; 36import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 37import com.android.inputmethod.latin.utils.CapsModeUtils; 38import com.android.inputmethod.latin.utils.DebugLogUtils; 39import com.android.inputmethod.latin.utils.PrevWordsInfoUtils; 40import com.android.inputmethod.latin.utils.ScriptUtils; 41import com.android.inputmethod.latin.utils.SpannableStringUtils; 42import com.android.inputmethod.latin.utils.StringUtils; 43import com.android.inputmethod.latin.utils.TextRange; 44 45import java.util.Arrays; 46 47/** 48 * Enrichment class for InputConnection to simplify interaction and add functionality. 49 * 50 * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying 51 * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC 52 * all the time to find out what text is in the buffer, when we need it to determine caps mode 53 * for example. 54 */ 55public final class RichInputConnection { 56 private static final String TAG = RichInputConnection.class.getSimpleName(); 57 private static final boolean DBG = false; 58 private static final boolean DEBUG_PREVIOUS_TEXT = false; 59 private static final boolean DEBUG_BATCH_NESTING = false; 60 // Provision for long words and separators between the words. 61 private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH 62 * (Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */ 63 + Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */; 64 private static final int INVALID_CURSOR_POSITION = -1; 65 66 /** 67 * This variable contains an expected value for the selection start position. This is where the 68 * cursor or selection start may end up after all the keyboard-triggered updates have passed. We 69 * keep this to compare it to the actual selection start to guess whether the move was caused by 70 * a keyboard command or not. 71 * It's not really the selection start position: the selection start may not be there yet, and 72 * in some cases, it may never arrive there. 73 */ 74 private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points 75 /** 76 * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is 77 * expected. The same caveats as mExpectedSelStart apply. 78 */ 79 private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points 80 /** 81 * This contains the committed text immediately preceding the cursor and the composing 82 * text if any. It is refreshed when the cursor moves by calling upon the TextView. 83 */ 84 private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); 85 /** 86 * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. 87 */ 88 private final StringBuilder mComposingText = new StringBuilder(); 89 90 /** 91 * This variable is a temporary object used in 92 * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation. 93 */ 94 private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder(); 95 /** 96 * This variable is used to track whether the last committed text had the background color or 97 * not. 98 * TODO: Omit this flag if possible. 99 */ 100 private boolean mLastCommittedTextHasBackgroundColor = false; 101 102 private final InputMethodService mParent; 103 InputConnection mIC; 104 int mNestLevel; 105 public RichInputConnection(final InputMethodService parent) { 106 mParent = parent; 107 mIC = null; 108 mNestLevel = 0; 109 } 110 111 private void checkConsistencyForDebug() { 112 final ExtractedTextRequest r = new ExtractedTextRequest(); 113 r.hintMaxChars = 0; 114 r.hintMaxLines = 0; 115 r.token = 1; 116 r.flags = 0; 117 final ExtractedText et = mIC.getExtractedText(r, 0); 118 final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 119 0); 120 final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText) 121 .append(mComposingText); 122 if (null == et || null == beforeCursor) return; 123 final int actualLength = Math.min(beforeCursor.length(), internal.length()); 124 if (internal.length() > actualLength) { 125 internal.delete(0, internal.length() - actualLength); 126 } 127 final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() 128 : beforeCursor.subSequence(beforeCursor.length() - actualLength, 129 beforeCursor.length()).toString(); 130 if (et.selectionStart != mExpectedSelStart 131 || !(reference.equals(internal.toString()))) { 132 final String context = "Expected selection start = " + mExpectedSelStart 133 + "\nActual selection start = " + et.selectionStart 134 + "\nExpected text = " + internal.length() + " " + internal 135 + "\nActual text = " + reference.length() + " " + reference; 136 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 137 } else { 138 Log.e(TAG, DebugLogUtils.getStackTrace(2)); 139 Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart); 140 } 141 } 142 143 public void beginBatchEdit() { 144 if (++mNestLevel == 1) { 145 mIC = mParent.getCurrentInputConnection(); 146 if (null != mIC) { 147 mIC.beginBatchEdit(); 148 } 149 } else { 150 if (DBG) { 151 throw new RuntimeException("Nest level too deep"); 152 } else { 153 Log.e(TAG, "Nest level too deep : " + mNestLevel); 154 } 155 } 156 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 157 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 158 } 159 160 public void endBatchEdit() { 161 if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead 162 if (--mNestLevel == 0 && null != mIC) { 163 mIC.endBatchEdit(); 164 } 165 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 166 } 167 168 /** 169 * Reset the cached text and retrieve it again from the editor. 170 * 171 * This should be called when the cursor moved. It's possible that we can't connect to 172 * the application when doing this; notably, this happens sometimes during rotation, probably 173 * because of a race condition in the framework. In this case, we just can't retrieve the 174 * data, so we empty the cache and note that we don't know the new cursor position, and we 175 * return false so that the caller knows about this and can retry later. 176 * 177 * @param newSelStart the new position of the selection start, as received from the system. 178 * @param newSelEnd the new position of the selection end, as received from the system. 179 * @param shouldFinishComposition whether we should finish the composition in progress. 180 * @return true if we were able to connect to the editor successfully, false otherwise. When 181 * this method returns false, the caches could not be correctly refreshed so they were only 182 * reset: the caller should try again later to return to normal operation. 183 */ 184 public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart, 185 final int newSelEnd, final boolean shouldFinishComposition) { 186 mExpectedSelStart = newSelStart; 187 mExpectedSelEnd = newSelEnd; 188 mComposingText.setLength(0); 189 final boolean didReloadTextSuccessfully = reloadTextCache(); 190 if (!didReloadTextSuccessfully) { 191 Log.d(TAG, "Will try to retrieve text later."); 192 return false; 193 } 194 if (null != mIC && shouldFinishComposition) { 195 mIC.finishComposingText(); 196 } 197 return true; 198 } 199 200 /** 201 * Reload the cached text from the InputConnection. 202 * 203 * @return true if successful 204 */ 205 private boolean reloadTextCache() { 206 mCommittedTextBeforeComposingText.setLength(0); 207 mIC = mParent.getCurrentInputConnection(); 208 // Call upon the inputconnection directly since our own method is using the cache, and 209 // we want to refresh it. 210 final CharSequence textBeforeCursor = null == mIC ? null : 211 mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); 212 if (null == textBeforeCursor) { 213 // For some reason the app thinks we are not connected to it. This looks like a 214 // framework bug... Fall back to ground state and return false. 215 mExpectedSelStart = INVALID_CURSOR_POSITION; 216 mExpectedSelEnd = INVALID_CURSOR_POSITION; 217 Log.e(TAG, "Unable to connect to the editor to retrieve text."); 218 return false; 219 } 220 mCommittedTextBeforeComposingText.append(textBeforeCursor); 221 return true; 222 } 223 224 private void checkBatchEdit() { 225 if (mNestLevel != 1) { 226 // TODO: exception instead 227 Log.e(TAG, "Batch edit level incorrect : " + mNestLevel); 228 Log.e(TAG, DebugLogUtils.getStackTrace(4)); 229 } 230 } 231 232 public void finishComposingText() { 233 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 234 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 235 // TODO: this is not correct! The cursor is not necessarily after the composing text. 236 // In the practice right now this is only called when input ends so it will be reset so 237 // it works, but it's wrong and should be fixed. 238 mCommittedTextBeforeComposingText.append(mComposingText); 239 mComposingText.setLength(0); 240 // TODO: Clear this flag in setComposingRegion() and setComposingText() as well if needed. 241 mLastCommittedTextHasBackgroundColor = false; 242 if (null != mIC) { 243 mIC.finishComposingText(); 244 } 245 } 246 247 /** 248 * Synonym of {@code commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT}. 249 * @param text The text to commit. This may include styles. 250 * See {@link InputConnection#commitText(CharSequence, int)}. 251 * @param newCursorPosition The new cursor position around the text. 252 * See {@link InputConnection#commitText(CharSequence, int)}. 253 */ 254 public void commitText(final CharSequence text, final int newCursorPosition) { 255 commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT); 256 } 257 258 /** 259 * Calls {@link InputConnection#commitText(CharSequence, int)} with the given background color. 260 * @param text The text to commit. This may include styles. 261 * See {@link InputConnection#commitText(CharSequence, int)}. 262 * @param newCursorPosition The new cursor position around the text. 263 * See {@link InputConnection#commitText(CharSequence, int)}. 264 * @param color The background color to be attached. Set {@link Color#TRANSPARENT} to disable 265 * the background color. Note that this method specifies {@link BackgroundColorSpan} with 266 * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until 267 * {@link #finishComposingText()} is called. 268 */ 269 public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition, 270 final int color) { 271 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 272 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 273 mCommittedTextBeforeComposingText.append(text); 274 // TODO: the following is exceedingly error-prone. Right now when the cursor is in the 275 // middle of the composing word mComposingText only holds the part of the composing text 276 // that is before the cursor, so this actually works, but it's terribly confusing. Fix this. 277 mExpectedSelStart += text.length() - mComposingText.length(); 278 mExpectedSelEnd = mExpectedSelStart; 279 mComposingText.setLength(0); 280 mLastCommittedTextHasBackgroundColor = false; 281 if (null != mIC) { 282 if (color == Color.TRANSPARENT) { 283 mIC.commitText(text, newCursorPosition); 284 } else { 285 mTempObjectForCommitText.clear(); 286 mTempObjectForCommitText.append(text); 287 final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color); 288 mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, text.length(), 289 Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 290 mIC.commitText(mTempObjectForCommitText, newCursorPosition); 291 mLastCommittedTextHasBackgroundColor = true; 292 } 293 } 294 } 295 296 /** 297 * Removes the background color from the highlighted text if necessary. Should be called while 298 * there is no on-going composing text. 299 * 300 * <p>CAVEAT: This method internally calls {@link InputConnection#finishComposingText()}. 301 * Be careful of any unexpected side effects.</p> 302 */ 303 public void removeBackgroundColorFromHighlightedTextIfNecessary() { 304 // TODO: We haven't yet full tested if we really need to check this flag or not. Omit this 305 // flag if everything works fine without this condition. 306 if (!mLastCommittedTextHasBackgroundColor) { 307 return; 308 } 309 if (mComposingText.length() > 0) { 310 Log.e(TAG, "clearSpansWithComposingFlags should be called when composing text is " + 311 "empty. mComposingText=" + mComposingText); 312 return; 313 } 314 finishComposingText(); 315 } 316 317 public CharSequence getSelectedText(final int flags) { 318 return (null == mIC) ? null : mIC.getSelectedText(flags); 319 } 320 321 public boolean canDeleteCharacters() { 322 return mExpectedSelStart > 0; 323 } 324 325 /** 326 * Gets the caps modes we should be in after this specific string. 327 * 328 * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument. 329 * This method also supports faking an additional space after the string passed in argument, 330 * to support cases where a space will be added automatically, like in phantom space 331 * state for example. 332 * Note that for English, we are using American typography rules (which are not specific to 333 * American English, it's just the most common set of rules for English). 334 * 335 * @param inputType a mask of the caps modes to test for. 336 * @param spacingAndPunctuations the values of the settings to use for locale and separators. 337 * @param hasSpaceBefore if we should consider there should be a space after the string. 338 * @return the caps modes that should be on as a set of bits 339 */ 340 public int getCursorCapsMode(final int inputType, 341 final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { 342 mIC = mParent.getCurrentInputConnection(); 343 if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; 344 if (!TextUtils.isEmpty(mComposingText)) { 345 if (hasSpaceBefore) { 346 // If we have some composing text and a space before, then we should have 347 // MODE_CHARACTERS and MODE_WORDS on. 348 return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType; 349 } else { 350 // We have some composing text - we should be in MODE_CHARACTERS only. 351 return TextUtils.CAP_MODE_CHARACTERS & inputType; 352 } 353 } 354 // TODO: this will generally work, but there may be cases where the buffer contains SOME 355 // information but not enough to determine the caps mode accurately. This may happen after 356 // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. 357 // getCapsMode should be updated to be able to return a "not enough info" result so that 358 // we can get more context only when needed. 359 if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) { 360 if (!reloadTextCache()) { 361 Log.w(TAG, "Unable to connect to the editor. " 362 + "Setting caps mode without knowing text."); 363 } 364 } 365 // This never calls InputConnection#getCapsMode - in fact, it's a static method that 366 // never blocks or initiates IPC. 367 return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, 368 spacingAndPunctuations, hasSpaceBefore); 369 } 370 371 public int getCodePointBeforeCursor() { 372 final int length = mCommittedTextBeforeComposingText.length(); 373 if (length < 1) return Constants.NOT_A_CODE; 374 return Character.codePointBefore(mCommittedTextBeforeComposingText, length); 375 } 376 377 public CharSequence getTextBeforeCursor(final int n, final int flags) { 378 final int cachedLength = 379 mCommittedTextBeforeComposingText.length() + mComposingText.length(); 380 // If we have enough characters to satisfy the request, or if we have all characters in 381 // the text field, then we can return the cached version right away. 382 // However, if we don't have an expected cursor position, then we should always 383 // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to 384 // test for this explicitly) 385 if (INVALID_CURSOR_POSITION != mExpectedSelStart 386 && (cachedLength >= n || cachedLength >= mExpectedSelStart)) { 387 final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText); 388 // We call #toString() here to create a temporary object. 389 // In some situations, this method is called on a worker thread, and it's possible 390 // the main thread touches the contents of mComposingText while this worker thread 391 // is suspended, because mComposingText is a StringBuilder. This may lead to crashes, 392 // so we call #toString() on it. That will result in the return value being strictly 393 // speaking wrong, but since this is used for basing bigram probability off, and 394 // it's only going to matter for one getSuggestions call, it's fine in the practice. 395 s.append(mComposingText.toString()); 396 if (s.length() > n) { 397 s.delete(0, s.length() - n); 398 } 399 return s; 400 } 401 mIC = mParent.getCurrentInputConnection(); 402 return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags); 403 } 404 405 public CharSequence getTextAfterCursor(final int n, final int flags) { 406 mIC = mParent.getCurrentInputConnection(); 407 return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags); 408 } 409 410 public void deleteSurroundingText(final int beforeLength, final int afterLength) { 411 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 412 // TODO: the following is incorrect if the cursor is not immediately after the composition. 413 // Right now we never come here in this case because we reset the composing state before we 414 // come here in this case, but we need to fix this. 415 final int remainingChars = mComposingText.length() - beforeLength; 416 if (remainingChars >= 0) { 417 mComposingText.setLength(remainingChars); 418 } else { 419 mComposingText.setLength(0); 420 // Never cut under 0 421 final int len = Math.max(mCommittedTextBeforeComposingText.length() 422 + remainingChars, 0); 423 mCommittedTextBeforeComposingText.setLength(len); 424 } 425 if (mExpectedSelStart > beforeLength) { 426 mExpectedSelStart -= beforeLength; 427 mExpectedSelEnd -= beforeLength; 428 } else { 429 // There are fewer characters before the cursor in the buffer than we are being asked to 430 // delete. Only delete what is there, and update the end with the amount deleted. 431 mExpectedSelEnd -= mExpectedSelStart; 432 mExpectedSelStart = 0; 433 } 434 if (null != mIC) { 435 mIC.deleteSurroundingText(beforeLength, afterLength); 436 } 437 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 438 } 439 440 public void performEditorAction(final int actionId) { 441 mIC = mParent.getCurrentInputConnection(); 442 if (null != mIC) { 443 mIC.performEditorAction(actionId); 444 } 445 } 446 447 public void sendKeyEvent(final KeyEvent keyEvent) { 448 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 449 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 450 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 451 // This method is only called for enter or backspace when speaking to old applications 452 // (target SDK <= 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), or for digits. 453 // When talking to new applications we never use this method because it's inherently 454 // racy and has unpredictable results, but for backward compatibility we continue 455 // sending the key events for only Enter and Backspace because some applications 456 // mistakenly catch them to do some stuff. 457 switch (keyEvent.getKeyCode()) { 458 case KeyEvent.KEYCODE_ENTER: 459 mCommittedTextBeforeComposingText.append("\n"); 460 mExpectedSelStart += 1; 461 mExpectedSelEnd = mExpectedSelStart; 462 break; 463 case KeyEvent.KEYCODE_DEL: 464 if (0 == mComposingText.length()) { 465 if (mCommittedTextBeforeComposingText.length() > 0) { 466 mCommittedTextBeforeComposingText.delete( 467 mCommittedTextBeforeComposingText.length() - 1, 468 mCommittedTextBeforeComposingText.length()); 469 } 470 } else { 471 mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); 472 } 473 if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) { 474 // TODO: Handle surrogate pairs. 475 mExpectedSelStart -= 1; 476 } 477 mExpectedSelEnd = mExpectedSelStart; 478 break; 479 case KeyEvent.KEYCODE_UNKNOWN: 480 if (null != keyEvent.getCharacters()) { 481 mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); 482 mExpectedSelStart += keyEvent.getCharacters().length(); 483 mExpectedSelEnd = mExpectedSelStart; 484 } 485 break; 486 default: 487 final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar()); 488 mCommittedTextBeforeComposingText.append(text); 489 mExpectedSelStart += text.length(); 490 mExpectedSelEnd = mExpectedSelStart; 491 break; 492 } 493 } 494 if (null != mIC) { 495 mIC.sendKeyEvent(keyEvent); 496 } 497 } 498 499 public void setComposingRegion(final int start, final int end) { 500 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 501 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 502 final CharSequence textBeforeCursor = 503 getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0); 504 mCommittedTextBeforeComposingText.setLength(0); 505 if (!TextUtils.isEmpty(textBeforeCursor)) { 506 // The cursor is not necessarily at the end of the composing text, but we have its 507 // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start 508 // of the text, so we should use mExpectedSelStart. In other words, the composing 509 // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor 510 final int indexOfStartOfComposingText = 511 Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0); 512 mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, 513 textBeforeCursor.length())); 514 mCommittedTextBeforeComposingText.append( 515 textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); 516 } 517 if (null != mIC) { 518 mIC.setComposingRegion(start, end); 519 } 520 } 521 522 public void setComposingText(final CharSequence text, final int newCursorPosition) { 523 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 524 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 525 mExpectedSelStart += text.length() - mComposingText.length(); 526 mExpectedSelEnd = mExpectedSelStart; 527 mComposingText.setLength(0); 528 mComposingText.append(text); 529 // TODO: support values of newCursorPosition != 1. At this time, this is never called with 530 // newCursorPosition != 1. 531 if (null != mIC) { 532 mIC.setComposingText(text, newCursorPosition); 533 } 534 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 535 } 536 537 /** 538 * Set the selection of the text editor. 539 * 540 * Calls through to {@link InputConnection#setSelection(int, int)}. 541 * 542 * @param start the character index where the selection should start. 543 * @param end the character index where the selection should end. 544 * @return Returns true on success, false on failure: either the input connection is no longer 545 * valid when setting the selection or when retrieving the text cache at that point, or 546 * invalid arguments were passed. 547 */ 548 public boolean setSelection(final int start, final int end) { 549 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 550 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 551 if (start < 0 || end < 0) { 552 return false; 553 } 554 mExpectedSelStart = start; 555 mExpectedSelEnd = end; 556 if (null != mIC) { 557 final boolean isIcValid = mIC.setSelection(start, end); 558 if (!isIcValid) { 559 return false; 560 } 561 } 562 return reloadTextCache(); 563 } 564 565 public void commitCorrection(final CorrectionInfo correctionInfo) { 566 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 567 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 568 // This has no effect on the text field and does not change its content. It only makes 569 // TextView flash the text for a second based on indices contained in the argument. 570 if (null != mIC) { 571 mIC.commitCorrection(correctionInfo); 572 } 573 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 574 } 575 576 public void commitCompletion(final CompletionInfo completionInfo) { 577 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 578 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 579 CharSequence text = completionInfo.getText(); 580 // text should never be null, but just in case, it's better to insert nothing than to crash 581 if (null == text) text = ""; 582 mCommittedTextBeforeComposingText.append(text); 583 mExpectedSelStart += text.length() - mComposingText.length(); 584 mExpectedSelEnd = mExpectedSelStart; 585 mComposingText.setLength(0); 586 if (null != mIC) { 587 mIC.commitCompletion(completionInfo); 588 } 589 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 590 } 591 592 @SuppressWarnings("unused") 593 public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord( 594 final SpacingAndPunctuations spacingAndPunctuations, final int n) { 595 mIC = mParent.getCurrentInputConnection(); 596 if (null == mIC) { 597 return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; 598 } 599 final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); 600 if (DEBUG_PREVIOUS_TEXT && null != prev) { 601 final int checkLength = LOOKBACK_CHARACTER_NUM - 1; 602 final String reference = prev.length() <= checkLength ? prev.toString() 603 : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); 604 // TODO: right now the following works because mComposingText holds the part of the 605 // composing text that is before the cursor, but this is very confusing. We should 606 // fix it. 607 final StringBuilder internal = new StringBuilder() 608 .append(mCommittedTextBeforeComposingText).append(mComposingText); 609 if (internal.length() > checkLength) { 610 internal.delete(0, internal.length() - checkLength); 611 if (!(reference.equals(internal.toString()))) { 612 final String context = 613 "Expected text = " + internal + "\nActual text = " + reference; 614 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 615 } 616 } 617 } 618 return PrevWordsInfoUtils.getPrevWordsInfoFromNthPreviousWord( 619 prev, spacingAndPunctuations, n); 620 } 621 622 private static boolean isSeparator(final int code, final int[] sortedSeparators) { 623 return Arrays.binarySearch(sortedSeparators, code) >= 0; 624 } 625 626 /** 627 * Returns the text surrounding the cursor. 628 * 629 * @param sortedSeparators a sorted array of code points that split words. 630 * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_* 631 * @return a range containing the text surrounding the cursor 632 */ 633 public TextRange getWordRangeAtCursor(final int[] sortedSeparators, final int scriptId) { 634 mIC = mParent.getCurrentInputConnection(); 635 if (mIC == null) { 636 return null; 637 } 638 final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 639 InputConnection.GET_TEXT_WITH_STYLES); 640 final CharSequence after = mIC.getTextAfterCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 641 InputConnection.GET_TEXT_WITH_STYLES); 642 if (before == null || after == null) { 643 return null; 644 } 645 646 // Going backward, find the first breaking point (separator) 647 int startIndexInBefore = before.length(); 648 while (startIndexInBefore > 0) { 649 final int codePoint = Character.codePointBefore(before, startIndexInBefore); 650 if (isSeparator(codePoint, sortedSeparators) 651 || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { 652 break; 653 } 654 --startIndexInBefore; 655 if (Character.isSupplementaryCodePoint(codePoint)) { 656 --startIndexInBefore; 657 } 658 } 659 660 // Find last word separator after the cursor 661 int endIndexInAfter = -1; 662 while (++endIndexInAfter < after.length()) { 663 final int codePoint = Character.codePointAt(after, endIndexInAfter); 664 if (isSeparator(codePoint, sortedSeparators) 665 || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { 666 break; 667 } 668 if (Character.isSupplementaryCodePoint(codePoint)) { 669 ++endIndexInAfter; 670 } 671 } 672 673 final boolean hasUrlSpans = 674 SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length()) 675 || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter); 676 // We don't use TextUtils#concat because it copies all spans without respect to their 677 // nature. If the text includes a PARAGRAPH span and it has been split, then 678 // TextUtils#concat will crash when it tries to concat both sides of it. 679 return new TextRange( 680 SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after), 681 startIndexInBefore, before.length() + endIndexInAfter, before.length(), 682 hasUrlSpans); 683 } 684 685 public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { 686 if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { 687 // If what's after the cursor is a word character, then we're touching a word. 688 return true; 689 } 690 final String textBeforeCursor = mCommittedTextBeforeComposingText.toString(); 691 int indexOfCodePointInJavaChars = textBeforeCursor.length(); 692 int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE 693 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); 694 // Search for the first non word-connector char 695 if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) { 696 indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint); 697 consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE 698 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); 699 } 700 return !(Constants.NOT_A_CODE == consideredCodePoint 701 || spacingAndPunctuations.isWordSeparator(consideredCodePoint) 702 || spacingAndPunctuations.isWordConnector(consideredCodePoint)); 703 } 704 705 public boolean isCursorFollowedByWordCharacter( 706 final SpacingAndPunctuations spacingAndPunctuations) { 707 final CharSequence after = getTextAfterCursor(1, 0); 708 if (TextUtils.isEmpty(after)) { 709 return false; 710 } 711 final int codePointAfterCursor = Character.codePointAt(after, 0); 712 if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor) 713 || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) { 714 return false; 715 } 716 return true; 717 } 718 719 public void removeTrailingSpace() { 720 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 721 final int codePointBeforeCursor = getCodePointBeforeCursor(); 722 if (Constants.CODE_SPACE == codePointBeforeCursor) { 723 deleteSurroundingText(1, 0); 724 } 725 } 726 727 public boolean sameAsTextBeforeCursor(final CharSequence text) { 728 final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); 729 return TextUtils.equals(text, beforeText); 730 } 731 732 public boolean revertDoubleSpacePeriod() { 733 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 734 // Here we test whether we indeed have a period and a space before us. This should not 735 // be needed, but it's there just in case something went wrong. 736 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 737 if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) { 738 // Theoretically we should not be coming here if there isn't ". " before the 739 // cursor, but the application may be changing the text while we are typing, so 740 // anything goes. We should not crash. 741 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 742 + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor."); 743 return false; 744 } 745 // Double-space results in ". ". A backspace to cancel this should result in a single 746 // space in the text field, so we replace ". " with a single space. 747 deleteSurroundingText(2, 0); 748 final String singleSpace = " "; 749 commitText(singleSpace, 1); 750 return true; 751 } 752 753 public boolean revertSwapPunctuation() { 754 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 755 // Here we test whether we indeed have a space and something else before us. This should not 756 // be needed, but it's there just in case something went wrong. 757 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 758 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 759 // enter surrogate pairs this code will have been removed. 760 if (TextUtils.isEmpty(textBeforeCursor) 761 || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) { 762 // We may only come here if the application is changing the text while we are typing. 763 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 764 // but some debugging log may be in order. 765 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 766 + "find a space just before the cursor."); 767 return false; 768 } 769 deleteSurroundingText(2, 0); 770 final String text = " " + textBeforeCursor.subSequence(0, 1); 771 commitText(text, 1); 772 return true; 773 } 774 775 /** 776 * Heuristic to determine if this is an expected update of the cursor. 777 * 778 * Sometimes updates to the cursor position are late because of their asynchronous nature. 779 * This method tries to determine if this update is one, based on the values of the cursor 780 * position in the update, and the currently expected position of the cursor according to 781 * LatinIME's internal accounting. If this is not a belated expected update, then it should 782 * mean that the user moved the cursor explicitly. 783 * This is quite robust, but of course it's not perfect. In particular, it will fail in the 784 * case we get an update A, the user types in N characters so as to move the cursor to A+N but 785 * we don't get those, and then the user places the cursor between A and A+N, and we get only 786 * this update and not the ones in-between. This is almost impossible to achieve even trying 787 * very very hard. 788 * 789 * @param oldSelStart The value of the old selection in the update. 790 * @param newSelStart The value of the new selection in the update. 791 * @param oldSelEnd The value of the old selection end in the update. 792 * @param newSelEnd The value of the new selection end in the update. 793 * @return whether this is a belated expected update or not. 794 */ 795 public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart, 796 final int oldSelEnd, final int newSelEnd) { 797 // This update is "belated" if we are expecting it. That is, mExpectedSelStart and 798 // mExpectedSelEnd match the new values that the TextView is updating TO. 799 if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true; 800 // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old 801 // values, and one of newSelStart or newSelEnd is updated to a different value. In this 802 // case, it is likely that something other than the IME has moved the selection endpoint 803 // to the new value. 804 if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd 805 && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false; 806 // If neither of the above two cases hold, then the system may be having trouble keeping up 807 // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart 808 // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then 809 // assume a belated update. 810 return (newSelStart == newSelEnd) 811 && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0 812 && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0; 813 } 814 815 /** 816 * Looks at the text just before the cursor to find out if it looks like a URL. 817 * 818 * The weakest point here is, if we don't have enough text bufferized, we may fail to realize 819 * we are in URL situation, but other places in this class have the same limitation and it 820 * does not matter too much in the practice. 821 */ 822 public boolean textBeforeCursorLooksLikeURL() { 823 return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); 824 } 825 826 /** 827 * Looks at the text just before the cursor to find out if we are inside a double quote. 828 * 829 * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. 830 * However this won't be a concrete problem in most situations, as the cache is almost always 831 * long enough for this use. 832 */ 833 public boolean isInsideDoubleQuoteOrAfterDigit() { 834 return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); 835 } 836 837 /** 838 * Try to get the text from the editor to expose lies the framework may have been 839 * telling us. Concretely, when the device rotates, the frameworks tells us about where the 840 * cursor used to be initially in the editor at the time it first received the focus; this 841 * may be completely different from the place it is upon rotation. Since we don't have any 842 * means to get the real value, try at least to ask the text view for some characters and 843 * detect the most damaging cases: when the cursor position is declared to be much smaller 844 * than it really is. 845 */ 846 public void tryFixLyingCursorPosition() { 847 final CharSequence textBeforeCursor = getTextBeforeCursor( 848 Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); 849 if (null == textBeforeCursor) { 850 mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION; 851 } else { 852 final int textLength = textBeforeCursor.length(); 853 if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE 854 && (textLength > mExpectedSelStart 855 || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { 856 // It should not be possible to have only one of those variables be 857 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized 858 // (simple cursor, no selection) or there is no cursor/we don't know its pos 859 final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd; 860 mExpectedSelStart = textLength; 861 // We can't figure out the value of mLastSelectionEnd :( 862 // But at least if it's smaller than mLastSelectionStart something is wrong, 863 // and if they used to be equal we also don't want to make it look like there is a 864 // selection. 865 if (wasEqual || mExpectedSelStart > mExpectedSelEnd) { 866 mExpectedSelEnd = mExpectedSelStart; 867 } 868 } 869 } 870 } 871 872 public int getExpectedSelectionStart() { 873 return mExpectedSelStart; 874 } 875 876 public int getExpectedSelectionEnd() { 877 return mExpectedSelEnd; 878 } 879 880 /** 881 * @return whether there is a selection currently active. 882 */ 883 public boolean hasSelection() { 884 return mExpectedSelEnd != mExpectedSelStart; 885 } 886 887 public boolean isCursorPositionKnown() { 888 return INVALID_CURSOR_POSITION != mExpectedSelStart; 889 } 890 891 /** 892 * Work around a bug that was present before Jelly Bean upon rotation. 893 * 894 * Before Jelly Bean, there is a bug where setComposingRegion and other committing 895 * functions on the input connection get ignored until the cursor moves. This method works 896 * around the bug by wiggling the cursor first, which reactivates the connection and has 897 * the subsequent methods work, then restoring it to its original position. 898 * 899 * On platforms on which this method is not present, this is a no-op. 900 */ 901 public void maybeMoveTheCursorAroundAndRestoreToWorkaroundABug() { 902 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { 903 if (mExpectedSelStart > 0) { 904 mIC.setSelection(mExpectedSelStart - 1, mExpectedSelStart - 1); 905 } else { 906 mIC.setSelection(mExpectedSelStart + 1, mExpectedSelStart + 1); 907 } 908 mIC.setSelection(mExpectedSelStart, mExpectedSelEnd); 909 } 910 } 911 912 private boolean mCursorAnchorInfoMonitorEnabled = false; 913 914 /** 915 * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}. 916 * @param enableMonitor {@code true} to request the editor to call back the method whenever the 917 * cursor/anchor position is changed. 918 * @param requestImmediateCallback {@code true} to request the editor to call back the method 919 * as soon as possible to notify the current cursor/anchor position to the input method. 920 * @return {@code true} if the request is accepted. Returns {@code false} otherwise, which 921 * includes "not implemented" or "rejected" or "temporarily unavailable" or whatever which 922 * prevents the application from fulfilling the request. (TODO: Improve the API when it turns 923 * out that we actually need more detailed error codes) 924 */ 925 public boolean requestUpdateCursorAnchorInfo(final boolean enableMonitor, 926 final boolean requestImmediateCallback) { 927 mIC = mParent.getCurrentInputConnection(); 928 final boolean scheduled; 929 if (null != mIC) { 930 scheduled = InputConnectionCompatUtils.requestUpdateCursorAnchorInfo(mIC, 931 enableMonitor, requestImmediateCallback); 932 } else { 933 scheduled = false; 934 } 935 mCursorAnchorInfoMonitorEnabled = (scheduled && enableMonitor); 936 return scheduled; 937 } 938 939 /** 940 * @return {@code true} if the application reported that the monitor mode of 941 * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled. 942 */ 943 public boolean isCursorAnchorInfoMonitorEnabled() { 944 return mCursorAnchorInfoMonitorEnabled; 945 } 946} 947