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