RichInputConnection.java revision e5dee0af4ae991988919b2c5b80c10a94b26884e
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.inputmethodservice.InputMethodService; 20import android.text.SpannableString; 21import android.text.TextUtils; 22import android.util.Log; 23import android.view.KeyEvent; 24import android.view.inputmethod.CompletionInfo; 25import android.view.inputmethod.CorrectionInfo; 26import android.view.inputmethod.ExtractedText; 27import android.view.inputmethod.ExtractedTextRequest; 28import android.view.inputmethod.InputConnection; 29 30import com.android.inputmethod.latin.define.ProductionFlag; 31import com.android.inputmethod.research.ResearchLogger; 32 33import java.util.Locale; 34import 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 */ 44public 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 final CharSequence textBeforeCursor = 342 getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0); 343 mCommittedTextBeforeComposingText.setLength(0); 344 if (!TextUtils.isEmpty(textBeforeCursor)) { 345 final int indexOfStartOfComposingText = 346 Math.max(textBeforeCursor.length() - (end - start), 0); 347 mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, 348 textBeforeCursor.length())); 349 mCommittedTextBeforeComposingText.append( 350 textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); 351 } 352 if (null != mIC) { 353 mIC.setComposingRegion(start, end); 354 } 355 } 356 357 public void setComposingText(final CharSequence text, final int i) { 358 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 359 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 360 mCurrentCursorPosition += text.length() - mComposingText.length(); 361 mComposingText.setLength(0); 362 mComposingText.append(text); 363 // TODO: support values of i != 1. At this time, this is never called with i != 1. 364 if (null != mIC) { 365 mIC.setComposingText(text, i); 366 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 367 ResearchLogger.richInputConnection_setComposingText(text, i); 368 } 369 } 370 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 371 } 372 373 public void setSelection(final int from, final int to) { 374 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 375 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 376 if (null != mIC) { 377 mIC.setSelection(from, to); 378 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 379 ResearchLogger.richInputConnection_setSelection(from, to); 380 } 381 } 382 mCurrentCursorPosition = from; 383 mCommittedTextBeforeComposingText.setLength(0); 384 mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); 385 } 386 387 public void commitCorrection(final CorrectionInfo correctionInfo) { 388 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 389 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 390 // This has no effect on the text field and does not change its content. It only makes 391 // TextView flash the text for a second based on indices contained in the argument. 392 if (null != mIC) { 393 mIC.commitCorrection(correctionInfo); 394 } 395 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 396 } 397 398 public void commitCompletion(final CompletionInfo completionInfo) { 399 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 400 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 401 CharSequence text = completionInfo.getText(); 402 // text should never be null, but just in case, it's better to insert nothing than to crash 403 if (null == text) text = ""; 404 mCommittedTextBeforeComposingText.append(text); 405 mCurrentCursorPosition += text.length() - mComposingText.length(); 406 mComposingText.setLength(0); 407 if (null != mIC) { 408 mIC.commitCompletion(completionInfo); 409 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 410 ResearchLogger.richInputConnection_commitCompletion(completionInfo); 411 } 412 } 413 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 414 } 415 416 @SuppressWarnings("unused") 417 public String getNthPreviousWord(final String sentenceSeperators, final int n) { 418 mIC = mParent.getCurrentInputConnection(); 419 if (null == mIC) return null; 420 final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); 421 if (DEBUG_PREVIOUS_TEXT && null != prev) { 422 final int checkLength = LOOKBACK_CHARACTER_NUM - 1; 423 final String reference = prev.length() <= checkLength ? prev.toString() 424 : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); 425 final StringBuilder internal = new StringBuilder() 426 .append(mCommittedTextBeforeComposingText).append(mComposingText); 427 if (internal.length() > checkLength) { 428 internal.delete(0, internal.length() - checkLength); 429 if (!(reference.equals(internal.toString()))) { 430 final String context = 431 "Expected text = " + internal + "\nActual text = " + reference; 432 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 433 } 434 } 435 } 436 return getNthPreviousWord(prev, sentenceSeperators, n); 437 } 438 439 /** 440 * Represents a range of text, relative to the current cursor position. 441 */ 442 public static final class Range { 443 /** Characters before selection start */ 444 public final int mCharsBefore; 445 446 /** 447 * Characters after selection start, including one trailing word 448 * separator. 449 */ 450 public final int mCharsAfter; 451 452 /** The actual characters that make up a word */ 453 public final CharSequence mWord; 454 455 public Range(int charsBefore, int charsAfter, CharSequence word) { 456 if (charsBefore < 0 || charsAfter < 0) { 457 throw new IndexOutOfBoundsException(); 458 } 459 this.mCharsBefore = charsBefore; 460 this.mCharsAfter = charsAfter; 461 this.mWord = word; 462 } 463 } 464 465 private static boolean isSeparator(int code, String sep) { 466 return sep.indexOf(code) != -1; 467 } 468 469 // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor, 470 // n = 2 retrieves the word before that, and so on. This splits on whitespace only. 471 // Also, it won't return words that end in a separator (if the nth word before the cursor 472 // ends in a separator, it returns null). 473 // Example : 474 // (n = 1) "abc def|" -> def 475 // (n = 1) "abc def |" -> def 476 // (n = 1) "abc def. |" -> null 477 // (n = 1) "abc def . |" -> null 478 // (n = 2) "abc def|" -> abc 479 // (n = 2) "abc def |" -> abc 480 // (n = 2) "abc def. |" -> abc 481 // (n = 2) "abc def . |" -> def 482 // (n = 2) "abc|" -> null 483 // (n = 2) "abc |" -> null 484 // (n = 2) "abc. def|" -> null 485 public static String getNthPreviousWord(final CharSequence prev, 486 final String sentenceSeperators, final int n) { 487 if (prev == null) return null; 488 final String[] w = spaceRegex.split(prev); 489 490 // If we can't find n words, or we found an empty word, return null. 491 if (w.length < n) return null; 492 final String nthPrevWord = w[w.length - n]; 493 final int length = nthPrevWord.length(); 494 if (length <= 0) return null; 495 496 // If ends in a separator, return null 497 final char lastChar = nthPrevWord.charAt(length - 1); 498 if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; 499 500 return nthPrevWord; 501 } 502 503 /** 504 * @param separators characters which may separate words 505 * @return the word that surrounds the cursor, including up to one trailing 506 * separator. For example, if the field contains "he|llo world", where | 507 * represents the cursor, then "hello " will be returned. 508 */ 509 public CharSequence getWordAtCursor(String separators) { 510 // getWordRangeAtCursor returns null if the connection is null 511 Range r = getWordRangeAtCursor(separators, 0); 512 return (r == null) ? null : r.mWord; 513 } 514 515 /** 516 * Returns the text surrounding the cursor. 517 * 518 * @param sep a string of characters that split words. 519 * @param additionalPrecedingWordsCount the number of words before the current word that should 520 * be included in the returned range 521 * @return a range containing the text surrounding the cursor 522 */ 523 public Range getWordRangeAtCursor(final String sep, final int additionalPrecedingWordsCount) { 524 mIC = mParent.getCurrentInputConnection(); 525 if (mIC == null || sep == null) { 526 return null; 527 } 528 final CharSequence before = mIC.getTextBeforeCursor(1000, 529 InputConnection.GET_TEXT_WITH_STYLES); 530 final CharSequence after = mIC.getTextAfterCursor(1000, 531 InputConnection.GET_TEXT_WITH_STYLES); 532 if (before == null || after == null) { 533 return null; 534 } 535 536 // Going backward, alternate skipping non-separators and separators until enough words 537 // have been read. 538 int count = additionalPrecedingWordsCount; 539 int startIndexInBefore = before.length(); 540 boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at 541 while (true) { // see comments below for why this is guaranteed to halt 542 while (startIndexInBefore > 0) { 543 final int codePoint = Character.codePointBefore(before, startIndexInBefore); 544 if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) { 545 break; // inner loop 546 } 547 --startIndexInBefore; 548 if (Character.isSupplementaryCodePoint(codePoint)) { 549 --startIndexInBefore; 550 } 551 } 552 // isStoppingAtWhitespace is true every other time through the loop, 553 // so additionalPrecedingWordsCount is guaranteed to become < 0, which 554 // guarantees outer loop termination 555 if (isStoppingAtWhitespace && (--count < 0)) { 556 break; // outer loop 557 } 558 isStoppingAtWhitespace = !isStoppingAtWhitespace; 559 } 560 561 // Find last word separator after the cursor 562 int endIndexInAfter = -1; 563 while (++endIndexInAfter < after.length()) { 564 final int codePoint = Character.codePointAt(after, endIndexInAfter); 565 if (isSeparator(codePoint, sep)) { 566 break; 567 } 568 if (Character.isSupplementaryCodePoint(codePoint)) { 569 ++endIndexInAfter; 570 } 571 } 572 573 final SpannableString word = new SpannableString(TextUtils.concat( 574 before.subSequence(startIndexInBefore, before.length()), 575 after.subSequence(0, endIndexInAfter))); 576 return new Range(before.length() - startIndexInBefore, endIndexInAfter, word); 577 } 578 579 public boolean isCursorTouchingWord(final SettingsValues settingsValues) { 580 final CharSequence before = getTextBeforeCursor(1, 0); 581 final CharSequence after = getTextAfterCursor(1, 0); 582 if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0)) 583 && !settingsValues.isWordConnector(before.charAt(0))) { 584 return true; 585 } 586 if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0)) 587 && !settingsValues.isWordConnector(after.charAt(0))) { 588 return true; 589 } 590 return false; 591 } 592 593 public void removeTrailingSpace() { 594 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 595 final CharSequence lastOne = getTextBeforeCursor(1, 0); 596 if (lastOne != null && lastOne.length() == 1 597 && lastOne.charAt(0) == Constants.CODE_SPACE) { 598 deleteSurroundingText(1, 0); 599 } 600 } 601 602 public boolean sameAsTextBeforeCursor(final CharSequence text) { 603 final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); 604 return TextUtils.equals(text, beforeText); 605 } 606 607 /* (non-javadoc) 608 * Returns the word before the cursor if the cursor is at the end of a word, null otherwise 609 */ 610 public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) { 611 // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, 612 // separator or end of line/text) 613 // Example: "test|"<EOL> "te|st" get rejected here 614 final CharSequence textAfterCursor = getTextAfterCursor(1, 0); 615 if (!TextUtils.isEmpty(textAfterCursor) 616 && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null; 617 618 // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) 619 // Example: " -|" gets rejected here but "e-|" and "e|" are okay 620 CharSequence word = getWordAtCursor(settings.mWordSeparators); 621 // We don't suggest on leading single quotes, so we have to remove them from the word if 622 // it starts with single quotes. 623 while (!TextUtils.isEmpty(word) && Constants.CODE_SINGLE_QUOTE == word.charAt(0)) { 624 word = word.subSequence(1, word.length()); 625 } 626 if (TextUtils.isEmpty(word)) return null; 627 // Find the last code point of the string 628 final int lastCodePoint = Character.codePointBefore(word, word.length()); 629 // If for some reason the text field contains non-unicode binary data, or if the 630 // charsequence is exactly one char long and the contents is a low surrogate, return null. 631 if (!Character.isDefined(lastCodePoint)) return null; 632 // Bail out if the cursor is not at the end of a word (cursor must be preceded by 633 // non-whitespace, non-separator, non-start-of-text) 634 // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. 635 if (settings.isWordSeparator(lastCodePoint)) return null; 636 final char firstChar = word.charAt(0); // we just tested that word is not empty 637 if (word.length() == 1 && !Character.isLetter(firstChar)) return null; 638 639 // We don't restart suggestion if the first character is not a letter, because we don't 640 // start composing when the first character is not a letter. 641 if (!Character.isLetter(firstChar)) return null; 642 643 return word; 644 } 645 646 public boolean revertDoubleSpacePeriod() { 647 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 648 // Here we test whether we indeed have a period and a space before us. This should not 649 // be needed, but it's there just in case something went wrong. 650 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 651 final String periodSpace = ". "; 652 if (!periodSpace.equals(textBeforeCursor)) { 653 // Theoretically we should not be coming here if there isn't ". " before the 654 // cursor, but the application may be changing the text while we are typing, so 655 // anything goes. We should not crash. 656 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 657 + "\"" + periodSpace + "\" just before the cursor."); 658 return false; 659 } 660 deleteSurroundingText(2, 0); 661 final String doubleSpace = " "; 662 commitText(doubleSpace, 1); 663 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 664 ResearchLogger.richInputConnection_revertDoubleSpacePeriod(); 665 } 666 return true; 667 } 668 669 public boolean revertSwapPunctuation() { 670 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 671 // Here we test whether we indeed have a space and something else before us. This should not 672 // be needed, but it's there just in case something went wrong. 673 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 674 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 675 // enter surrogate pairs this code will have been removed. 676 if (TextUtils.isEmpty(textBeforeCursor) 677 || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) { 678 // We may only come here if the application is changing the text while we are typing. 679 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 680 // but some debugging log may be in order. 681 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 682 + "find a space just before the cursor."); 683 return false; 684 } 685 deleteSurroundingText(2, 0); 686 final String text = " " + textBeforeCursor.subSequence(0, 1); 687 commitText(text, 1); 688 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 689 ResearchLogger.richInputConnection_revertSwapPunctuation(); 690 } 691 return true; 692 } 693 694 /** 695 * Heuristic to determine if this is an expected update of the cursor. 696 * 697 * Sometimes updates to the cursor position are late because of their asynchronous nature. 698 * This method tries to determine if this update is one, based on the values of the cursor 699 * position in the update, and the currently expected position of the cursor according to 700 * LatinIME's internal accounting. If this is not a belated expected update, then it should 701 * mean that the user moved the cursor explicitly. 702 * This is quite robust, but of course it's not perfect. In particular, it will fail in the 703 * case we get an update A, the user types in N characters so as to move the cursor to A+N but 704 * we don't get those, and then the user places the cursor between A and A+N, and we get only 705 * this update and not the ones in-between. This is almost impossible to achieve even trying 706 * very very hard. 707 * 708 * @param oldSelStart The value of the old cursor position in the update. 709 * @param newSelStart The value of the new cursor position in the update. 710 * @return whether this is a belated expected update or not. 711 */ 712 public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) { 713 // If this is an update that arrives at our expected position, it's a belated update. 714 if (newSelStart == mCurrentCursorPosition) return true; 715 // If this is an update that moves the cursor from our expected position, it must be 716 // an explicit move. 717 if (oldSelStart == mCurrentCursorPosition) return false; 718 // The following returns true if newSelStart is between oldSelStart and 719 // mCurrentCursorPosition. We assume that if the updated position is between the old 720 // position and the expected position, then it must be a belated update. 721 return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0; 722 } 723 724 /** 725 * The user moved the cursor by hand. Take a note of it. 726 * @param newCursorPosition The new cursor position. 727 */ 728 public void userMovedCursor(final int newCursorPosition) { 729 mCurrentCursorPosition = newCursorPosition; 730 } 731 732 /** 733 * Looks at the text just before the cursor to find out if it looks like a URL. 734 * 735 * The weakest point here is, if we don't have enough text bufferized, we may fail to realize 736 * we are in URL situation, but other places in this class have the same limitation and it 737 * does not matter too much in the practice. 738 */ 739 public boolean textBeforeCursorLooksLikeURL() { 740 return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); 741 } 742} 743