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