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