RichInputConnection.java revision 96b22200beb98fd1a6288f4cf53e38611a09cdd0
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.inputmethodservice.InputMethodService; 20import android.text.TextUtils; 21import android.util.Log; 22import android.view.KeyEvent; 23import android.view.inputmethod.CompletionInfo; 24import android.view.inputmethod.CorrectionInfo; 25import android.view.inputmethod.ExtractedText; 26import android.view.inputmethod.ExtractedTextRequest; 27import android.view.inputmethod.InputConnection; 28 29import com.android.inputmethod.latin.define.ProductionFlag; 30import com.android.inputmethod.research.ResearchLogger; 31 32import java.util.Locale; 33import java.util.regex.Pattern; 34 35/** 36 * Enrichment class for InputConnection to simplify interaction and add functionality. 37 * 38 * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying 39 * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC 40 * all the time to find out what text is in the buffer, when we need it to determine caps mode 41 * for example. 42 */ 43public final class RichInputConnection { 44 private static final String TAG = RichInputConnection.class.getSimpleName(); 45 private static final boolean DBG = false; 46 private static final boolean DEBUG_PREVIOUS_TEXT = false; 47 private static final boolean DEBUG_BATCH_NESTING = false; 48 // Provision for a long word pair and a separator 49 private static final int LOOKBACK_CHARACTER_NUM = Constants.Dictionary.MAX_WORD_LENGTH * 2 + 1; 50 private static final Pattern spaceRegex = Pattern.compile("\\s+"); 51 private static final int INVALID_CURSOR_POSITION = -1; 52 53 /** 54 * This variable contains the value LatinIME thinks the cursor position should be at now. 55 * This is a few steps in advance of what the TextView thinks it is, because TextView will 56 * only know after the IPC calls gets through. 57 */ 58 private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points 59 /** 60 * This contains the committed text immediately preceding the cursor and the composing 61 * text if any. It is refreshed when the cursor moves by calling upon the TextView. 62 */ 63 private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); 64 /** 65 * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. 66 */ 67 private StringBuilder mComposingText = new StringBuilder(); 68 // A hint on how many characters to cache from the TextView. A good value of this is given by 69 // how many characters we need to be able to almost always find the caps mode. 70 private static final int DEFAULT_TEXT_CACHE_SIZE = 100; 71 72 private final InputMethodService mParent; 73 InputConnection mIC; 74 int mNestLevel; 75 public RichInputConnection(final InputMethodService parent) { 76 mParent = parent; 77 mIC = null; 78 mNestLevel = 0; 79 } 80 81 private void checkConsistencyForDebug() { 82 final ExtractedTextRequest r = new ExtractedTextRequest(); 83 r.hintMaxChars = 0; 84 r.hintMaxLines = 0; 85 r.token = 1; 86 r.flags = 0; 87 final ExtractedText et = mIC.getExtractedText(r, 0); 88 final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0); 89 final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText) 90 .append(mComposingText); 91 if (null == et || null == beforeCursor) return; 92 final int actualLength = Math.min(beforeCursor.length(), internal.length()); 93 if (internal.length() > actualLength) { 94 internal.delete(0, internal.length() - actualLength); 95 } 96 final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() 97 : beforeCursor.subSequence(beforeCursor.length() - actualLength, 98 beforeCursor.length()).toString(); 99 if (et.selectionStart != mCurrentCursorPosition 100 || !(reference.equals(internal.toString()))) { 101 final String context = "Expected cursor position = " + mCurrentCursorPosition 102 + "\nActual cursor position = " + et.selectionStart 103 + "\nExpected text = " + internal.length() + " " + internal 104 + "\nActual text = " + reference.length() + " " + reference; 105 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 106 } else { 107 Log.e(TAG, Utils.getStackTrace(2)); 108 Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart); 109 } 110 } 111 112 public void beginBatchEdit() { 113 if (++mNestLevel == 1) { 114 mIC = mParent.getCurrentInputConnection(); 115 if (null != mIC) { 116 mIC.beginBatchEdit(); 117 } 118 } else { 119 if (DBG) { 120 throw new RuntimeException("Nest level too deep"); 121 } else { 122 Log.e(TAG, "Nest level too deep : " + mNestLevel); 123 } 124 } 125 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 126 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 127 } 128 129 public void endBatchEdit() { 130 if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead 131 if (--mNestLevel == 0 && null != mIC) { 132 mIC.endBatchEdit(); 133 } 134 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 135 } 136 137 public void resetCachesUponCursorMove(final int newCursorPosition) { 138 mCurrentCursorPosition = newCursorPosition; 139 mComposingText.setLength(0); 140 mCommittedTextBeforeComposingText.setLength(0); 141 final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0); 142 if (null != textBeforeCursor) mCommittedTextBeforeComposingText.append(textBeforeCursor); 143 if (null != mIC) { 144 mIC.finishComposingText(); 145 if (ProductionFlag.IS_EXPERIMENTAL) { 146 ResearchLogger.richInputConnection_finishComposingText(); 147 } 148 } 149 } 150 151 private void checkBatchEdit() { 152 if (mNestLevel != 1) { 153 // TODO: exception instead 154 Log.e(TAG, "Batch edit level incorrect : " + mNestLevel); 155 Log.e(TAG, Utils.getStackTrace(4)); 156 } 157 } 158 159 public void finishComposingText() { 160 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 161 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 162 mCommittedTextBeforeComposingText.append(mComposingText); 163 mCurrentCursorPosition += mComposingText.length(); 164 mComposingText.setLength(0); 165 if (null != mIC) { 166 mIC.finishComposingText(); 167 if (ProductionFlag.IS_EXPERIMENTAL) { 168 ResearchLogger.richInputConnection_finishComposingText(); 169 } 170 } 171 } 172 173 public void commitText(final CharSequence text, final int i) { 174 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 175 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 176 mCommittedTextBeforeComposingText.append(text); 177 mCurrentCursorPosition += text.length() - mComposingText.length(); 178 mComposingText.setLength(0); 179 if (null != mIC) { 180 mIC.commitText(text, i); 181 if (ProductionFlag.IS_EXPERIMENTAL) { 182 ResearchLogger.richInputConnection_commitText(text, i); 183 } 184 } 185 } 186 187 /** 188 * Gets the caps modes we should be in after this specific string. 189 * 190 * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument. 191 * This method also supports faking an additional space after the string passed in argument, 192 * to support cases where a space will be added automatically, like in phantom space 193 * state for example. 194 * Note that for English, we are using American typography rules (which are not specific to 195 * American English, it's just the most common set of rules for English). 196 * 197 * @param inputType a mask of the caps modes to test for. 198 * @param locale what language should be considered. 199 * @param hasSpaceBefore if we should consider there should be a space after the string. 200 * @return the caps modes that should be on as a set of bits 201 */ 202 public int getCursorCapsMode(final int inputType, final Locale locale, 203 final boolean hasSpaceBefore) { 204 mIC = mParent.getCurrentInputConnection(); 205 if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; 206 if (!TextUtils.isEmpty(mComposingText)) { 207 if (hasSpaceBefore) { 208 // If we have some composing text and a space before, then we should have 209 // MODE_CHARACTERS and MODE_WORDS on. 210 return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType; 211 } else { 212 // We have some composing text - we should be in MODE_CHARACTERS only. 213 return TextUtils.CAP_MODE_CHARACTERS & inputType; 214 } 215 } 216 // TODO: this will generally work, but there may be cases where the buffer contains SOME 217 // information but not enough to determine the caps mode accurately. This may happen after 218 // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. 219 // getCapsMode should be updated to be able to return a "not enough info" result so that 220 // we can get more context only when needed. 221 if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mCurrentCursorPosition) { 222 mCommittedTextBeforeComposingText.append( 223 getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); 224 } 225 // This never calls InputConnection#getCapsMode - in fact, it's a static method that 226 // never blocks or initiates IPC. 227 return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale, 228 hasSpaceBefore); 229 } 230 231 public int getCodePointBeforeCursor() { 232 if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE; 233 return Character.codePointBefore(mCommittedTextBeforeComposingText, 234 mCommittedTextBeforeComposingText.length()); 235 } 236 237 public CharSequence getTextBeforeCursor(final int i, final int j) { 238 // TODO: use mCommittedTextBeforeComposingText if possible to improve performance 239 mIC = mParent.getCurrentInputConnection(); 240 if (null != mIC) return mIC.getTextBeforeCursor(i, j); 241 return null; 242 } 243 244 public CharSequence getTextAfterCursor(final int i, final int j) { 245 mIC = mParent.getCurrentInputConnection(); 246 if (null != mIC) return mIC.getTextAfterCursor(i, j); 247 return null; 248 } 249 250 public void deleteSurroundingText(final int i, final int j) { 251 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 252 final int remainingChars = mComposingText.length() - i; 253 if (remainingChars >= 0) { 254 mComposingText.setLength(remainingChars); 255 } else { 256 mComposingText.setLength(0); 257 // Never cut under 0 258 final int len = Math.max(mCommittedTextBeforeComposingText.length() 259 + remainingChars, 0); 260 mCommittedTextBeforeComposingText.setLength(len); 261 } 262 if (mCurrentCursorPosition > i) { 263 mCurrentCursorPosition -= i; 264 } else { 265 mCurrentCursorPosition = 0; 266 } 267 if (null != mIC) { 268 mIC.deleteSurroundingText(i, j); 269 if (ProductionFlag.IS_EXPERIMENTAL) { 270 ResearchLogger.richInputConnection_deleteSurroundingText(i, j); 271 } 272 } 273 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 274 } 275 276 public void performEditorAction(final int actionId) { 277 mIC = mParent.getCurrentInputConnection(); 278 if (null != mIC) { 279 mIC.performEditorAction(actionId); 280 if (ProductionFlag.IS_EXPERIMENTAL) { 281 ResearchLogger.richInputConnection_performEditorAction(actionId); 282 } 283 } 284 } 285 286 public void sendKeyEvent(final KeyEvent keyEvent) { 287 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 288 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 289 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 290 // This method is only called for enter or backspace when speaking to old 291 // applications (target SDK <= 15), or for digits. 292 // When talking to new applications we never use this method because it's inherently 293 // racy and has unpredictable results, but for backward compatibility we continue 294 // sending the key events for only Enter and Backspace because some applications 295 // mistakenly catch them to do some stuff. 296 switch (keyEvent.getKeyCode()) { 297 case KeyEvent.KEYCODE_ENTER: 298 mCommittedTextBeforeComposingText.append("\n"); 299 mCurrentCursorPosition += 1; 300 break; 301 case KeyEvent.KEYCODE_DEL: 302 if (0 == mComposingText.length()) { 303 if (mCommittedTextBeforeComposingText.length() > 0) { 304 mCommittedTextBeforeComposingText.delete( 305 mCommittedTextBeforeComposingText.length() - 1, 306 mCommittedTextBeforeComposingText.length()); 307 } 308 } else { 309 mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); 310 } 311 if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1; 312 break; 313 case KeyEvent.KEYCODE_UNKNOWN: 314 if (null != keyEvent.getCharacters()) { 315 mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); 316 mCurrentCursorPosition += keyEvent.getCharacters().length(); 317 } 318 break; 319 default: 320 final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1); 321 mCommittedTextBeforeComposingText.append(text); 322 mCurrentCursorPosition += text.length(); 323 break; 324 } 325 } 326 if (null != mIC) { 327 mIC.sendKeyEvent(keyEvent); 328 if (ProductionFlag.IS_EXPERIMENTAL) { 329 ResearchLogger.richInputConnection_sendKeyEvent(keyEvent); 330 } 331 } 332 } 333 334 public void setComposingRegion(final int start, final int end) { 335 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 336 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 337 mCurrentCursorPosition = end; 338 final CharSequence textBeforeCursor = 339 getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0); 340 final int indexOfStartOfComposingText = 341 Math.max(textBeforeCursor.length() - (end - start), 0); 342 mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, 343 textBeforeCursor.length())); 344 mCommittedTextBeforeComposingText.setLength(0); 345 mCommittedTextBeforeComposingText.append( 346 textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); 347 if (null != mIC) { 348 mIC.setComposingRegion(start, end); 349 } 350 } 351 352 public void setComposingText(final CharSequence text, final int i) { 353 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 354 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 355 mCurrentCursorPosition += text.length() - mComposingText.length(); 356 mComposingText.setLength(0); 357 mComposingText.append(text); 358 // TODO: support values of i != 1. At this time, this is never called with i != 1. 359 if (null != mIC) { 360 mIC.setComposingText(text, i); 361 if (ProductionFlag.IS_EXPERIMENTAL) { 362 ResearchLogger.richInputConnection_setComposingText(text, i); 363 } 364 } 365 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 366 } 367 368 public void setSelection(final int from, final int to) { 369 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 370 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 371 if (null != mIC) { 372 mIC.setSelection(from, to); 373 if (ProductionFlag.IS_EXPERIMENTAL) { 374 ResearchLogger.richInputConnection_setSelection(from, to); 375 } 376 } 377 mCurrentCursorPosition = from; 378 mCommittedTextBeforeComposingText.setLength(0); 379 mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); 380 } 381 382 public void commitCorrection(final CorrectionInfo correctionInfo) { 383 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 384 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 385 // This has no effect on the text field and does not change its content. It only makes 386 // TextView flash the text for a second based on indices contained in the argument. 387 if (null != mIC) { 388 mIC.commitCorrection(correctionInfo); 389 if (ProductionFlag.IS_EXPERIMENTAL) { 390 ResearchLogger.richInputConnection_commitCorrection(correctionInfo); 391 } 392 } 393 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 394 } 395 396 public void commitCompletion(final CompletionInfo completionInfo) { 397 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 398 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 399 final CharSequence text = completionInfo.getText(); 400 mCommittedTextBeforeComposingText.append(text); 401 mCurrentCursorPosition += text.length() - mComposingText.length(); 402 mComposingText.setLength(0); 403 if (null != mIC) { 404 mIC.commitCompletion(completionInfo); 405 if (ProductionFlag.IS_EXPERIMENTAL) { 406 ResearchLogger.richInputConnection_commitCompletion(completionInfo); 407 } 408 } 409 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 410 } 411 412 @SuppressWarnings("unused") 413 public String getNthPreviousWord(final String sentenceSeperators, final int n) { 414 mIC = mParent.getCurrentInputConnection(); 415 if (null == mIC) return null; 416 final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); 417 if (DEBUG_PREVIOUS_TEXT && null != prev) { 418 final int checkLength = LOOKBACK_CHARACTER_NUM - 1; 419 final String reference = prev.length() <= checkLength ? prev.toString() 420 : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); 421 final StringBuilder internal = new StringBuilder() 422 .append(mCommittedTextBeforeComposingText).append(mComposingText); 423 if (internal.length() > checkLength) { 424 internal.delete(0, internal.length() - checkLength); 425 if (!(reference.equals(internal.toString()))) { 426 final String context = 427 "Expected text = " + internal + "\nActual text = " + reference; 428 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 429 } 430 } 431 } 432 return getNthPreviousWord(prev, sentenceSeperators, n); 433 } 434 435 /** 436 * Represents a range of text, relative to the current cursor position. 437 */ 438 public static final class Range { 439 /** Characters before selection start */ 440 public final int mCharsBefore; 441 442 /** 443 * Characters after selection start, including one trailing word 444 * separator. 445 */ 446 public final int mCharsAfter; 447 448 /** The actual characters that make up a word */ 449 public final String mWord; 450 451 public Range(int charsBefore, int charsAfter, String word) { 452 if (charsBefore < 0 || charsAfter < 0) { 453 throw new IndexOutOfBoundsException(); 454 } 455 this.mCharsBefore = charsBefore; 456 this.mCharsAfter = charsAfter; 457 this.mWord = word; 458 } 459 } 460 461 private static boolean isSeparator(int code, String sep) { 462 return sep.indexOf(code) != -1; 463 } 464 465 // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor, 466 // n = 2 retrieves the word before that, and so on. This splits on whitespace only. 467 // Also, it won't return words that end in a separator (if the nth word before the cursor 468 // ends in a separator, it returns null). 469 // Example : 470 // (n = 1) "abc def|" -> def 471 // (n = 1) "abc def |" -> def 472 // (n = 1) "abc def. |" -> null 473 // (n = 1) "abc def . |" -> null 474 // (n = 2) "abc def|" -> abc 475 // (n = 2) "abc def |" -> abc 476 // (n = 2) "abc def. |" -> abc 477 // (n = 2) "abc def . |" -> def 478 // (n = 2) "abc|" -> null 479 // (n = 2) "abc |" -> null 480 // (n = 2) "abc. def|" -> null 481 public static String getNthPreviousWord(final CharSequence prev, 482 final String sentenceSeperators, final int n) { 483 if (prev == null) return null; 484 final String[] w = spaceRegex.split(prev); 485 486 // If we can't find n words, or we found an empty word, return null. 487 if (w.length < n) return null; 488 final String nthPrevWord = w[w.length - n]; 489 final int length = nthPrevWord.length(); 490 if (length <= 0) return null; 491 492 // If ends in a separator, return null 493 final char lastChar = nthPrevWord.charAt(length - 1); 494 if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; 495 496 return nthPrevWord; 497 } 498 499 /** 500 * @param separators characters which may separate words 501 * @return the word that surrounds the cursor, including up to one trailing 502 * separator. For example, if the field contains "he|llo world", where | 503 * represents the cursor, then "hello " will be returned. 504 */ 505 public String getWordAtCursor(String separators) { 506 // getWordRangeAtCursor returns null if the connection is null 507 Range r = getWordRangeAtCursor(separators, 0); 508 return (r == null) ? null : r.mWord; 509 } 510 511 private int getCursorPosition() { 512 mIC = mParent.getCurrentInputConnection(); 513 if (null == mIC) return INVALID_CURSOR_POSITION; 514 final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0); 515 if (extracted == null) { 516 return INVALID_CURSOR_POSITION; 517 } 518 return extracted.startOffset + extracted.selectionStart; 519 } 520 521 /** 522 * Returns the text surrounding the cursor. 523 * 524 * @param sep a string of characters that split words. 525 * @param additionalPrecedingWordsCount the number of words before the current word that should 526 * be included in the returned range 527 * @return a range containing the text surrounding the cursor 528 */ 529 public Range getWordRangeAtCursor(final String sep, final int additionalPrecedingWordsCount) { 530 mIC = mParent.getCurrentInputConnection(); 531 if (mIC == null || sep == null) { 532 return null; 533 } 534 final CharSequence before = mIC.getTextBeforeCursor(1000, 0); 535 final CharSequence after = mIC.getTextAfterCursor(1000, 0); 536 if (before == null || after == null) { 537 return null; 538 } 539 540 // Going backward, alternate skipping non-separators and separators until enough words 541 // have been read. 542 int count = additionalPrecedingWordsCount; 543 int start = before.length(); 544 boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at 545 while (true) { // see comments below for why this is guaranteed to halt 546 while (start > 0) { 547 final int codePoint = Character.codePointBefore(before, start); 548 if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) { 549 break; // inner loop 550 } 551 --start; 552 if (Character.isSupplementaryCodePoint(codePoint)) { 553 --start; 554 } 555 } 556 // isStoppingAtWhitespace is true every other time through the loop, 557 // so additionalPrecedingWordsCount is guaranteed to become < 0, which 558 // guarantees outer loop termination 559 if (isStoppingAtWhitespace && (--count < 0)) { 560 break; // outer loop 561 } 562 isStoppingAtWhitespace = !isStoppingAtWhitespace; 563 } 564 565 // Find last word separator after the cursor 566 int end = -1; 567 while (++end < after.length()) { 568 final int codePoint = Character.codePointAt(after, end); 569 if (isSeparator(codePoint, sep)) { 570 break; 571 } 572 if (Character.isSupplementaryCodePoint(codePoint)) { 573 ++end; 574 } 575 } 576 577 final int cursor = getCursorPosition(); 578 if (start >= 0 && cursor + end <= after.length() + before.length()) { 579 String word = before.toString().substring(start, before.length()) 580 + after.toString().substring(0, end); 581 return new Range(before.length() - start, end, word); 582 } 583 584 return null; 585 } 586 587 public boolean isCursorTouchingWord(final SettingsValues settingsValues) { 588 final CharSequence before = getTextBeforeCursor(1, 0); 589 final CharSequence after = getTextAfterCursor(1, 0); 590 if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0)) 591 && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) { 592 return true; 593 } 594 if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0)) 595 && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) { 596 return true; 597 } 598 return false; 599 } 600 601 public void removeTrailingSpace() { 602 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 603 final CharSequence lastOne = getTextBeforeCursor(1, 0); 604 if (lastOne != null && lastOne.length() == 1 605 && lastOne.charAt(0) == Constants.CODE_SPACE) { 606 deleteSurroundingText(1, 0); 607 } 608 } 609 610 public boolean sameAsTextBeforeCursor(final CharSequence text) { 611 final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); 612 return TextUtils.equals(text, beforeText); 613 } 614 615 /* (non-javadoc) 616 * Returns the word before the cursor if the cursor is at the end of a word, null otherwise 617 */ 618 public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) { 619 // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, 620 // separator or end of line/text) 621 // Example: "test|"<EOL> "te|st" get rejected here 622 final CharSequence textAfterCursor = getTextAfterCursor(1, 0); 623 if (!TextUtils.isEmpty(textAfterCursor) 624 && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null; 625 626 // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) 627 // Example: " -|" gets rejected here but "e-|" and "e|" are okay 628 CharSequence word = getWordAtCursor(settings.mWordSeparators); 629 // We don't suggest on leading single quotes, so we have to remove them from the word if 630 // it starts with single quotes. 631 while (!TextUtils.isEmpty(word) && Constants.CODE_SINGLE_QUOTE == word.charAt(0)) { 632 word = word.subSequence(1, word.length()); 633 } 634 if (TextUtils.isEmpty(word)) return null; 635 // Find the last code point of the string 636 final int lastCodePoint = Character.codePointBefore(word, word.length()); 637 // If for some reason the text field contains non-unicode binary data, or if the 638 // charsequence is exactly one char long and the contents is a low surrogate, return null. 639 if (!Character.isDefined(lastCodePoint)) return null; 640 // Bail out if the cursor is not at the end of a word (cursor must be preceded by 641 // non-whitespace, non-separator, non-start-of-text) 642 // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. 643 if (settings.isWordSeparator(lastCodePoint)) return null; 644 final char firstChar = word.charAt(0); // we just tested that word is not empty 645 if (word.length() == 1 && !Character.isLetter(firstChar)) return null; 646 647 // We only suggest on words that start with a letter or a symbol that is excluded from 648 // word separators (see #handleCharacterWhileInBatchEdit). 649 if (!(Character.isLetter(firstChar) 650 || settings.isSymbolExcludedFromWordSeparators(firstChar))) { 651 return null; 652 } 653 654 return word; 655 } 656 657 public boolean revertDoubleSpace() { 658 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 659 // Here we test whether we indeed have a period and a space before us. This should not 660 // be needed, but it's there just in case something went wrong. 661 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 662 if (!". ".equals(textBeforeCursor)) { 663 // Theoretically we should not be coming here if there isn't ". " before the 664 // cursor, but the application may be changing the text while we are typing, so 665 // anything goes. We should not crash. 666 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 667 + "\". \" just before the cursor."); 668 return false; 669 } 670 deleteSurroundingText(2, 0); 671 commitText(" ", 1); 672 return true; 673 } 674 675 public boolean revertSwapPunctuation() { 676 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 677 // Here we test whether we indeed have a space and something else before us. This should not 678 // be needed, but it's there just in case something went wrong. 679 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 680 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 681 // enter surrogate pairs this code will have been removed. 682 if (TextUtils.isEmpty(textBeforeCursor) 683 || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) { 684 // We may only come here if the application is changing the text while we are typing. 685 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 686 // but some debugging log may be in order. 687 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 688 + "find a space just before the cursor."); 689 return false; 690 } 691 deleteSurroundingText(2, 0); 692 commitText(" " + textBeforeCursor.subSequence(0, 1), 1); 693 return true; 694 } 695 696 /** 697 * Heuristic to determine if this is an expected update of the cursor. 698 * 699 * Sometimes updates to the cursor position are late because of their asynchronous nature. 700 * This method tries to determine if this update is one, based on the values of the cursor 701 * position in the update, and the currently expected position of the cursor according to 702 * LatinIME's internal accounting. If this is not a belated expected update, then it should 703 * mean that the user moved the cursor explicitly. 704 * This is quite robust, but of course it's not perfect. In particular, it will fail in the 705 * case we get an update A, the user types in N characters so as to move the cursor to A+N but 706 * we don't get those, and then the user places the cursor between A and A+N, and we get only 707 * this update and not the ones in-between. This is almost impossible to achieve even trying 708 * very very hard. 709 * 710 * @param oldSelStart The value of the old cursor position in the update. 711 * @param newSelStart The value of the new cursor position in the update. 712 * @return whether this is a belated expected update or not. 713 */ 714 public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) { 715 // If this is an update that arrives at our expected position, it's a belated update. 716 if (newSelStart == mCurrentCursorPosition) return true; 717 // If this is an update that moves the cursor from our expected position, it must be 718 // an explicit move. 719 if (oldSelStart == mCurrentCursorPosition) return false; 720 // The following returns true if newSelStart is between oldSelStart and 721 // mCurrentCursorPosition. We assume that if the updated position is between the old 722 // position and the expected position, then it must be a belated update. 723 return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0; 724 } 725} 726