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