RichInputConnection.java revision b6695867a5fe6af999d23c669e2e6a6182457bba
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.inputmethodservice.InputMethodService; 20import android.text.TextUtils; 21import android.util.Log; 22import android.view.KeyEvent; 23import android.view.inputmethod.CompletionInfo; 24import android.view.inputmethod.CorrectionInfo; 25import android.view.inputmethod.ExtractedText; 26import android.view.inputmethod.ExtractedTextRequest; 27import android.view.inputmethod.InputConnection; 28 29import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; 30import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 31import com.android.inputmethod.latin.utils.CapsModeUtils; 32import com.android.inputmethod.latin.utils.DebugLogUtils; 33import com.android.inputmethod.latin.utils.SpannableStringUtils; 34import com.android.inputmethod.latin.utils.StringUtils; 35import com.android.inputmethod.latin.utils.TextRange; 36 37import java.util.Arrays; 38import java.util.regex.Pattern; 39 40/** 41 * Enrichment class for InputConnection to simplify interaction and add functionality. 42 * 43 * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying 44 * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC 45 * all the time to find out what text is in the buffer, when we need it to determine caps mode 46 * for example. 47 */ 48public final class RichInputConnection { 49 private static final String TAG = RichInputConnection.class.getSimpleName(); 50 private static final boolean DBG = false; 51 private static final boolean DEBUG_PREVIOUS_TEXT = false; 52 private static final boolean DEBUG_BATCH_NESTING = false; 53 // Provision for long words and separators between the words. 54 private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH 55 * (Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */ 56 + Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */; 57 private static final Pattern spaceRegex = Pattern.compile("\\s+"); 58 private static final int INVALID_CURSOR_POSITION = -1; 59 60 /** 61 * This variable contains an expected value for the selection start position. This is where the 62 * cursor or selection start may end up after all the keyboard-triggered updates have passed. We 63 * keep this to compare it to the actual selection start to guess whether the move was caused by 64 * a keyboard command or not. 65 * It's not really the selection start position: the selection start may not be there yet, and 66 * in some cases, it may never arrive there. 67 */ 68 private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points 69 /** 70 * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is 71 * expected. The same caveats as mExpectedSelStart apply. 72 */ 73 private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points 74 /** 75 * This contains the committed text immediately preceding the cursor and the composing 76 * text if any. It is refreshed when the cursor moves by calling upon the TextView. 77 */ 78 private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); 79 /** 80 * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. 81 */ 82 private final StringBuilder mComposingText = new StringBuilder(); 83 84 private final InputMethodService mParent; 85 InputConnection mIC; 86 int mNestLevel; 87 public RichInputConnection(final InputMethodService parent) { 88 mParent = parent; 89 mIC = null; 90 mNestLevel = 0; 91 } 92 93 private void checkConsistencyForDebug() { 94 final ExtractedTextRequest r = new ExtractedTextRequest(); 95 r.hintMaxChars = 0; 96 r.hintMaxLines = 0; 97 r.token = 1; 98 r.flags = 0; 99 final ExtractedText et = mIC.getExtractedText(r, 0); 100 final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 101 0); 102 final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText) 103 .append(mComposingText); 104 if (null == et || null == beforeCursor) return; 105 final int actualLength = Math.min(beforeCursor.length(), internal.length()); 106 if (internal.length() > actualLength) { 107 internal.delete(0, internal.length() - actualLength); 108 } 109 final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() 110 : beforeCursor.subSequence(beforeCursor.length() - actualLength, 111 beforeCursor.length()).toString(); 112 if (et.selectionStart != mExpectedSelStart 113 || !(reference.equals(internal.toString()))) { 114 final String context = "Expected selection start = " + mExpectedSelStart 115 + "\nActual selection start = " + et.selectionStart 116 + "\nExpected text = " + internal.length() + " " + internal 117 + "\nActual text = " + reference.length() + " " + reference; 118 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 119 } else { 120 Log.e(TAG, DebugLogUtils.getStackTrace(2)); 121 Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart); 122 } 123 } 124 125 public void beginBatchEdit() { 126 if (++mNestLevel == 1) { 127 mIC = mParent.getCurrentInputConnection(); 128 if (null != mIC) { 129 mIC.beginBatchEdit(); 130 } 131 } else { 132 if (DBG) { 133 throw new RuntimeException("Nest level too deep"); 134 } else { 135 Log.e(TAG, "Nest level too deep : " + mNestLevel); 136 } 137 } 138 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 139 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 140 } 141 142 public void endBatchEdit() { 143 if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead 144 if (--mNestLevel == 0 && null != mIC) { 145 mIC.endBatchEdit(); 146 } 147 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 148 } 149 150 /** 151 * Reset the cached text and retrieve it again from the editor. 152 * 153 * This should be called when the cursor moved. It's possible that we can't connect to 154 * the application when doing this; notably, this happens sometimes during rotation, probably 155 * because of a race condition in the framework. In this case, we just can't retrieve the 156 * data, so we empty the cache and note that we don't know the new cursor position, and we 157 * return false so that the caller knows about this and can retry later. 158 * 159 * @param newSelStart the new position of the selection start, as received from the system. 160 * @param newSelEnd the new position of the selection end, as received from the system. 161 * @param shouldFinishComposition whether we should finish the composition in progress. 162 * @return true if we were able to connect to the editor successfully, false otherwise. When 163 * this method returns false, the caches could not be correctly refreshed so they were only 164 * reset: the caller should try again later to return to normal operation. 165 */ 166 public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart, 167 final int newSelEnd, final boolean shouldFinishComposition) { 168 mExpectedSelStart = newSelStart; 169 mExpectedSelEnd = newSelEnd; 170 mComposingText.setLength(0); 171 final boolean didReloadTextSuccessfully = reloadTextCache(); 172 if (!didReloadTextSuccessfully) { 173 Log.d(TAG, "Will try to retrieve text later."); 174 return false; 175 } 176 if (null != mIC && shouldFinishComposition) { 177 mIC.finishComposingText(); 178 } 179 return true; 180 } 181 182 /** 183 * Reload the cached text from the InputConnection. 184 * 185 * @return true if successful 186 */ 187 private boolean reloadTextCache() { 188 mCommittedTextBeforeComposingText.setLength(0); 189 mIC = mParent.getCurrentInputConnection(); 190 // Call upon the inputconnection directly since our own method is using the cache, and 191 // we want to refresh it. 192 final CharSequence textBeforeCursor = null == mIC ? null : 193 mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); 194 if (null == textBeforeCursor) { 195 // For some reason the app thinks we are not connected to it. This looks like a 196 // framework bug... Fall back to ground state and return false. 197 mExpectedSelStart = INVALID_CURSOR_POSITION; 198 mExpectedSelEnd = INVALID_CURSOR_POSITION; 199 Log.e(TAG, "Unable to connect to the editor to retrieve text."); 200 return false; 201 } 202 mCommittedTextBeforeComposingText.append(textBeforeCursor); 203 return true; 204 } 205 206 private void checkBatchEdit() { 207 if (mNestLevel != 1) { 208 // TODO: exception instead 209 Log.e(TAG, "Batch edit level incorrect : " + mNestLevel); 210 Log.e(TAG, DebugLogUtils.getStackTrace(4)); 211 } 212 } 213 214 public void finishComposingText() { 215 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 216 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 217 // TODO: this is not correct! The cursor is not necessarily after the composing text. 218 // In the practice right now this is only called when input ends so it will be reset so 219 // it works, but it's wrong and should be fixed. 220 mCommittedTextBeforeComposingText.append(mComposingText); 221 mComposingText.setLength(0); 222 if (null != mIC) { 223 mIC.finishComposingText(); 224 } 225 } 226 227 public void commitText(final CharSequence text, final int i) { 228 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 229 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 230 mCommittedTextBeforeComposingText.append(text); 231 // TODO: the following is exceedingly error-prone. Right now when the cursor is in the 232 // middle of the composing word mComposingText only holds the part of the composing text 233 // that is before the cursor, so this actually works, but it's terribly confusing. Fix this. 234 mExpectedSelStart += text.length() - mComposingText.length(); 235 mExpectedSelEnd = mExpectedSelStart; 236 mComposingText.setLength(0); 237 if (null != mIC) { 238 mIC.commitText(text, i); 239 } 240 } 241 242 public CharSequence getSelectedText(final int flags) { 243 return (null == mIC) ? null : mIC.getSelectedText(flags); 244 } 245 246 public boolean canDeleteCharacters() { 247 return mExpectedSelStart > 0; 248 } 249 250 /** 251 * Gets the caps modes we should be in after this specific string. 252 * 253 * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument. 254 * This method also supports faking an additional space after the string passed in argument, 255 * to support cases where a space will be added automatically, like in phantom space 256 * state for example. 257 * Note that for English, we are using American typography rules (which are not specific to 258 * American English, it's just the most common set of rules for English). 259 * 260 * @param inputType a mask of the caps modes to test for. 261 * @param spacingAndPunctuations the values of the settings to use for locale and separators. 262 * @param hasSpaceBefore if we should consider there should be a space after the string. 263 * @return the caps modes that should be on as a set of bits 264 */ 265 public int getCursorCapsMode(final int inputType, 266 final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { 267 mIC = mParent.getCurrentInputConnection(); 268 if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; 269 if (!TextUtils.isEmpty(mComposingText)) { 270 if (hasSpaceBefore) { 271 // If we have some composing text and a space before, then we should have 272 // MODE_CHARACTERS and MODE_WORDS on. 273 return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType; 274 } else { 275 // We have some composing text - we should be in MODE_CHARACTERS only. 276 return TextUtils.CAP_MODE_CHARACTERS & inputType; 277 } 278 } 279 // TODO: this will generally work, but there may be cases where the buffer contains SOME 280 // information but not enough to determine the caps mode accurately. This may happen after 281 // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. 282 // getCapsMode should be updated to be able to return a "not enough info" result so that 283 // we can get more context only when needed. 284 if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) { 285 if (!reloadTextCache()) { 286 Log.w(TAG, "Unable to connect to the editor. " 287 + "Setting caps mode without knowing text."); 288 } 289 } 290 // This never calls InputConnection#getCapsMode - in fact, it's a static method that 291 // never blocks or initiates IPC. 292 return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, 293 spacingAndPunctuations, hasSpaceBefore); 294 } 295 296 public int getCodePointBeforeCursor() { 297 final int length = mCommittedTextBeforeComposingText.length(); 298 if (length < 1) return Constants.NOT_A_CODE; 299 return Character.codePointBefore(mCommittedTextBeforeComposingText, length); 300 } 301 302 public CharSequence getTextBeforeCursor(final int n, final int flags) { 303 final int cachedLength = 304 mCommittedTextBeforeComposingText.length() + mComposingText.length(); 305 // If we have enough characters to satisfy the request, or if we have all characters in 306 // the text field, then we can return the cached version right away. 307 // However, if we don't have an expected cursor position, then we should always 308 // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to 309 // test for this explicitly) 310 if (INVALID_CURSOR_POSITION != mExpectedSelStart 311 && (cachedLength >= n || cachedLength >= mExpectedSelStart)) { 312 final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText); 313 // We call #toString() here to create a temporary object. 314 // In some situations, this method is called on a worker thread, and it's possible 315 // the main thread touches the contents of mComposingText while this worker thread 316 // is suspended, because mComposingText is a StringBuilder. This may lead to crashes, 317 // so we call #toString() on it. That will result in the return value being strictly 318 // speaking wrong, but since this is used for basing bigram probability off, and 319 // it's only going to matter for one getSuggestions call, it's fine in the practice. 320 s.append(mComposingText.toString()); 321 if (s.length() > n) { 322 s.delete(0, s.length() - n); 323 } 324 return s; 325 } 326 mIC = mParent.getCurrentInputConnection(); 327 return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags); 328 } 329 330 public CharSequence getTextAfterCursor(final int n, final int flags) { 331 mIC = mParent.getCurrentInputConnection(); 332 return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags); 333 } 334 335 public void deleteSurroundingText(final int beforeLength, final int afterLength) { 336 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 337 // TODO: the following is incorrect if the cursor is not immediately after the composition. 338 // Right now we never come here in this case because we reset the composing state before we 339 // come here in this case, but we need to fix this. 340 final int remainingChars = mComposingText.length() - beforeLength; 341 if (remainingChars >= 0) { 342 mComposingText.setLength(remainingChars); 343 } else { 344 mComposingText.setLength(0); 345 // Never cut under 0 346 final int len = Math.max(mCommittedTextBeforeComposingText.length() 347 + remainingChars, 0); 348 mCommittedTextBeforeComposingText.setLength(len); 349 } 350 if (mExpectedSelStart > beforeLength) { 351 mExpectedSelStart -= beforeLength; 352 mExpectedSelEnd -= beforeLength; 353 } else { 354 // There are fewer characters before the cursor in the buffer than we are being asked to 355 // delete. Only delete what is there, and update the end with the amount deleted. 356 mExpectedSelEnd -= mExpectedSelStart; 357 mExpectedSelStart = 0; 358 } 359 if (null != mIC) { 360 mIC.deleteSurroundingText(beforeLength, afterLength); 361 } 362 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 363 } 364 365 public void performEditorAction(final int actionId) { 366 mIC = mParent.getCurrentInputConnection(); 367 if (null != mIC) { 368 mIC.performEditorAction(actionId); 369 } 370 } 371 372 public void sendKeyEvent(final KeyEvent keyEvent) { 373 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 374 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 375 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 376 // This method is only called for enter or backspace when speaking to old applications 377 // (target SDK <= 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), or for digits. 378 // When talking to new applications we never use this method because it's inherently 379 // racy and has unpredictable results, but for backward compatibility we continue 380 // sending the key events for only Enter and Backspace because some applications 381 // mistakenly catch them to do some stuff. 382 switch (keyEvent.getKeyCode()) { 383 case KeyEvent.KEYCODE_ENTER: 384 mCommittedTextBeforeComposingText.append("\n"); 385 mExpectedSelStart += 1; 386 mExpectedSelEnd = mExpectedSelStart; 387 break; 388 case KeyEvent.KEYCODE_DEL: 389 if (0 == mComposingText.length()) { 390 if (mCommittedTextBeforeComposingText.length() > 0) { 391 mCommittedTextBeforeComposingText.delete( 392 mCommittedTextBeforeComposingText.length() - 1, 393 mCommittedTextBeforeComposingText.length()); 394 } 395 } else { 396 mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); 397 } 398 if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) { 399 // TODO: Handle surrogate pairs. 400 mExpectedSelStart -= 1; 401 } 402 mExpectedSelEnd = mExpectedSelStart; 403 break; 404 case KeyEvent.KEYCODE_UNKNOWN: 405 if (null != keyEvent.getCharacters()) { 406 mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); 407 mExpectedSelStart += keyEvent.getCharacters().length(); 408 mExpectedSelEnd = mExpectedSelStart; 409 } 410 break; 411 default: 412 final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar()); 413 mCommittedTextBeforeComposingText.append(text); 414 mExpectedSelStart += text.length(); 415 mExpectedSelEnd = mExpectedSelStart; 416 break; 417 } 418 } 419 if (null != mIC) { 420 mIC.sendKeyEvent(keyEvent); 421 } 422 } 423 424 public void setComposingRegion(final int start, final int end) { 425 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 426 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 427 final CharSequence textBeforeCursor = 428 getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0); 429 mCommittedTextBeforeComposingText.setLength(0); 430 if (!TextUtils.isEmpty(textBeforeCursor)) { 431 // The cursor is not necessarily at the end of the composing text, but we have its 432 // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start 433 // of the text, so we should use mExpectedSelStart. In other words, the composing 434 // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor 435 final int indexOfStartOfComposingText = 436 Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0); 437 mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, 438 textBeforeCursor.length())); 439 mCommittedTextBeforeComposingText.append( 440 textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); 441 } 442 if (null != mIC) { 443 mIC.setComposingRegion(start, end); 444 } 445 } 446 447 public void setComposingText(final CharSequence text, final int newCursorPosition) { 448 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 449 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 450 mExpectedSelStart += text.length() - mComposingText.length(); 451 mExpectedSelEnd = mExpectedSelStart; 452 mComposingText.setLength(0); 453 mComposingText.append(text); 454 // TODO: support values of newCursorPosition != 1. At this time, this is never called with 455 // newCursorPosition != 1. 456 if (null != mIC) { 457 mIC.setComposingText(text, newCursorPosition); 458 } 459 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 460 } 461 462 /** 463 * Set the selection of the text editor. 464 * 465 * Calls through to {@link InputConnection#setSelection(int, int)}. 466 * 467 * @param start the character index where the selection should start. 468 * @param end the character index where the selection should end. 469 * @return Returns true on success, false on failure: either the input connection is no longer 470 * valid when setting the selection or when retrieving the text cache at that point, or 471 * invalid arguments were passed. 472 */ 473 public boolean setSelection(final int start, final int end) { 474 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 475 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 476 if (start < 0 || end < 0) { 477 return false; 478 } 479 mExpectedSelStart = start; 480 mExpectedSelEnd = end; 481 if (null != mIC) { 482 final boolean isIcValid = mIC.setSelection(start, end); 483 if (!isIcValid) { 484 return false; 485 } 486 } 487 return reloadTextCache(); 488 } 489 490 public void commitCorrection(final CorrectionInfo correctionInfo) { 491 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 492 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 493 // This has no effect on the text field and does not change its content. It only makes 494 // TextView flash the text for a second based on indices contained in the argument. 495 if (null != mIC) { 496 mIC.commitCorrection(correctionInfo); 497 } 498 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 499 } 500 501 public void commitCompletion(final CompletionInfo completionInfo) { 502 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 503 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 504 CharSequence text = completionInfo.getText(); 505 // text should never be null, but just in case, it's better to insert nothing than to crash 506 if (null == text) text = ""; 507 mCommittedTextBeforeComposingText.append(text); 508 mExpectedSelStart += text.length() - mComposingText.length(); 509 mExpectedSelEnd = mExpectedSelStart; 510 mComposingText.setLength(0); 511 if (null != mIC) { 512 mIC.commitCompletion(completionInfo); 513 } 514 if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); 515 } 516 517 @SuppressWarnings("unused") 518 public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord( 519 final SpacingAndPunctuations spacingAndPunctuations, final int n) { 520 mIC = mParent.getCurrentInputConnection(); 521 if (null == mIC) { 522 return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; 523 } 524 final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); 525 if (DEBUG_PREVIOUS_TEXT && null != prev) { 526 final int checkLength = LOOKBACK_CHARACTER_NUM - 1; 527 final String reference = prev.length() <= checkLength ? prev.toString() 528 : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); 529 // TODO: right now the following works because mComposingText holds the part of the 530 // composing text that is before the cursor, but this is very confusing. We should 531 // fix it. 532 final StringBuilder internal = new StringBuilder() 533 .append(mCommittedTextBeforeComposingText).append(mComposingText); 534 if (internal.length() > checkLength) { 535 internal.delete(0, internal.length() - checkLength); 536 if (!(reference.equals(internal.toString()))) { 537 final String context = 538 "Expected text = " + internal + "\nActual text = " + reference; 539 ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); 540 } 541 } 542 } 543 return getPrevWordsInfoFromNthPreviousWord(prev, spacingAndPunctuations, n); 544 } 545 546 private static boolean isSeparator(final int code, final int[] sortedSeparators) { 547 return Arrays.binarySearch(sortedSeparators, code) >= 0; 548 } 549 550 // Get context information from nth word before the cursor. n = 1 retrieves the words 551 // immediately before the cursor, n = 2 retrieves the words before that, and so on. This splits 552 // on whitespace only. 553 // Also, it won't return words that end in a separator (if the nth word before the cursor 554 // ends in a separator, it returns information representing beginning-of-sentence). 555 // Example (when Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM is 2): 556 // (n = 1) "abc def|" -> abc, def 557 // (n = 1) "abc def |" -> abc, def 558 // (n = 1) "abc 'def|" -> empty, 'def 559 // (n = 1) "abc def. |" -> beginning-of-sentence 560 // (n = 1) "abc def . |" -> beginning-of-sentence 561 // (n = 2) "abc def|" -> beginning-of-sentence, abc 562 // (n = 2) "abc def |" -> beginning-of-sentence, abc 563 // (n = 2) "abc 'def|" -> empty. The context is different from "abc def", but we cannot 564 // represent this situation using PrevWordsInfo. See TODO in the method. 565 // TODO: The next example's result should be "abc, def". This have to be fixed before we 566 // retrieve the prior context of Beginning-of-Sentence. 567 // (n = 2) "abc def. |" -> beginning-of-sentence, abc 568 // (n = 2) "abc def . |" -> abc, def 569 // (n = 2) "abc|" -> beginning-of-sentence 570 // (n = 2) "abc |" -> beginning-of-sentence 571 // (n = 2) "abc. def|" -> beginning-of-sentence 572 public static PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(final CharSequence prev, 573 final SpacingAndPunctuations spacingAndPunctuations, final int n) { 574 if (prev == null) return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; 575 final String[] w = spaceRegex.split(prev); 576 final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; 577 for (int i = 0; i < prevWordsInfo.length; i++) { 578 final int focusedWordIndex = w.length - n - i; 579 // Referring to the word after the focused word. 580 if ((focusedWordIndex + 1) >= 0 && (focusedWordIndex + 1) < w.length) { 581 final String wordFollowingTheNthPrevWord = w[focusedWordIndex + 1]; 582 if (!wordFollowingTheNthPrevWord.isEmpty()) { 583 final char firstChar = wordFollowingTheNthPrevWord.charAt(0); 584 if (spacingAndPunctuations.isWordConnector(firstChar)) { 585 // The word following the focused word is starting with a word connector. 586 // TODO: Return meaningful context for this case. 587 prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; 588 break; 589 } 590 } 591 } 592 // If we can't find (n + i) words, the context is beginning-of-sentence. 593 if (focusedWordIndex < 0) { 594 prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; 595 break; 596 } 597 final String focusedWord = w[focusedWordIndex]; 598 // If the word is empty, the context is beginning-of-sentence. 599 final int length = focusedWord.length(); 600 if (length <= 0) { 601 prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; 602 break; 603 } 604 // If ends in a sentence separator, the context is beginning-of-sentence. 605 final char lastChar = focusedWord.charAt(length - 1); 606 if (spacingAndPunctuations.isSentenceSeparator(lastChar)) { 607 prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; 608 break; 609 } 610 // If ends in a word separator or connector, the context is unclear. 611 // TODO: Return meaningful context for this case. 612 if (spacingAndPunctuations.isWordSeparator(lastChar) 613 || spacingAndPunctuations.isWordConnector(lastChar)) { 614 prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; 615 break; 616 } 617 prevWordsInfo[i] = new WordInfo(focusedWord); 618 } 619 return new PrevWordsInfo(prevWordsInfo); 620 } 621 622 /** 623 * Returns the text surrounding the cursor. 624 * 625 * @param sortedSeparators a sorted array of code points that split words. 626 * @return a range containing the text surrounding the cursor 627 */ 628 public TextRange getWordRangeAtCursor(final int[] sortedSeparators) { 629 mIC = mParent.getCurrentInputConnection(); 630 if (mIC == null) { 631 return null; 632 } 633 final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 634 InputConnection.GET_TEXT_WITH_STYLES); 635 final CharSequence after = mIC.getTextAfterCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 636 InputConnection.GET_TEXT_WITH_STYLES); 637 if (before == null || after == null) { 638 return null; 639 } 640 641 // Going backward, find the first breaking point (separator) 642 int startIndexInBefore = before.length(); 643 while (startIndexInBefore > 0) { 644 final int codePoint = Character.codePointBefore(before, startIndexInBefore); 645 if (isSeparator(codePoint, sortedSeparators)) { 646 break; 647 } 648 --startIndexInBefore; 649 if (Character.isSupplementaryCodePoint(codePoint)) { 650 --startIndexInBefore; 651 } 652 } 653 654 // Find last word separator after the cursor 655 int endIndexInAfter = -1; 656 while (++endIndexInAfter < after.length()) { 657 final int codePoint = Character.codePointAt(after, endIndexInAfter); 658 if (isSeparator(codePoint, sortedSeparators)) { 659 break; 660 } 661 if (Character.isSupplementaryCodePoint(codePoint)) { 662 ++endIndexInAfter; 663 } 664 } 665 666 final boolean hasUrlSpans = 667 SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length()) 668 || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter); 669 // We don't use TextUtils#concat because it copies all spans without respect to their 670 // nature. If the text includes a PARAGRAPH span and it has been split, then 671 // TextUtils#concat will crash when it tries to concat both sides of it. 672 return new TextRange( 673 SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after), 674 startIndexInBefore, before.length() + endIndexInAfter, before.length(), 675 hasUrlSpans); 676 } 677 678 public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { 679 if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { 680 // If what's after the cursor is a word character, then we're touching a word. 681 return true; 682 } 683 final String textBeforeCursor = mCommittedTextBeforeComposingText.toString(); 684 int indexOfCodePointInJavaChars = textBeforeCursor.length(); 685 int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE 686 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); 687 // Search for the first non word-connector char 688 if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) { 689 indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint); 690 consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE 691 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); 692 } 693 return !(Constants.NOT_A_CODE == consideredCodePoint 694 || spacingAndPunctuations.isWordSeparator(consideredCodePoint) 695 || spacingAndPunctuations.isWordConnector(consideredCodePoint)); 696 } 697 698 public boolean isCursorFollowedByWordCharacter( 699 final SpacingAndPunctuations spacingAndPunctuations) { 700 final CharSequence after = getTextAfterCursor(1, 0); 701 if (TextUtils.isEmpty(after)) { 702 return false; 703 } 704 final int codePointAfterCursor = Character.codePointAt(after, 0); 705 if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor) 706 || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) { 707 return false; 708 } 709 return true; 710 } 711 712 public void removeTrailingSpace() { 713 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 714 final int codePointBeforeCursor = getCodePointBeforeCursor(); 715 if (Constants.CODE_SPACE == codePointBeforeCursor) { 716 deleteSurroundingText(1, 0); 717 } 718 } 719 720 public boolean sameAsTextBeforeCursor(final CharSequence text) { 721 final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); 722 return TextUtils.equals(text, beforeText); 723 } 724 725 public boolean revertDoubleSpacePeriod() { 726 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 727 // Here we test whether we indeed have a period and a space before us. This should not 728 // be needed, but it's there just in case something went wrong. 729 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 730 if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) { 731 // Theoretically we should not be coming here if there isn't ". " before the 732 // cursor, but the application may be changing the text while we are typing, so 733 // anything goes. We should not crash. 734 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 735 + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor."); 736 return false; 737 } 738 // Double-space results in ". ". A backspace to cancel this should result in a single 739 // space in the text field, so we replace ". " with a single space. 740 deleteSurroundingText(2, 0); 741 final String singleSpace = " "; 742 commitText(singleSpace, 1); 743 return true; 744 } 745 746 public boolean revertSwapPunctuation() { 747 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 748 // Here we test whether we indeed have a space and something else before us. This should not 749 // be needed, but it's there just in case something went wrong. 750 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 751 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 752 // enter surrogate pairs this code will have been removed. 753 if (TextUtils.isEmpty(textBeforeCursor) 754 || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) { 755 // We may only come here if the application is changing the text while we are typing. 756 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 757 // but some debugging log may be in order. 758 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 759 + "find a space just before the cursor."); 760 return false; 761 } 762 deleteSurroundingText(2, 0); 763 final String text = " " + textBeforeCursor.subSequence(0, 1); 764 commitText(text, 1); 765 return true; 766 } 767 768 /** 769 * Heuristic to determine if this is an expected update of the cursor. 770 * 771 * Sometimes updates to the cursor position are late because of their asynchronous nature. 772 * This method tries to determine if this update is one, based on the values of the cursor 773 * position in the update, and the currently expected position of the cursor according to 774 * LatinIME's internal accounting. If this is not a belated expected update, then it should 775 * mean that the user moved the cursor explicitly. 776 * This is quite robust, but of course it's not perfect. In particular, it will fail in the 777 * case we get an update A, the user types in N characters so as to move the cursor to A+N but 778 * we don't get those, and then the user places the cursor between A and A+N, and we get only 779 * this update and not the ones in-between. This is almost impossible to achieve even trying 780 * very very hard. 781 * 782 * @param oldSelStart The value of the old selection in the update. 783 * @param newSelStart The value of the new selection in the update. 784 * @param oldSelEnd The value of the old selection end in the update. 785 * @param newSelEnd The value of the new selection end in the update. 786 * @return whether this is a belated expected update or not. 787 */ 788 public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart, 789 final int oldSelEnd, final int newSelEnd) { 790 // This update is "belated" if we are expecting it. That is, mExpectedSelStart and 791 // mExpectedSelEnd match the new values that the TextView is updating TO. 792 if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true; 793 // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old 794 // values, and one of newSelStart or newSelEnd is updated to a different value. In this 795 // case, it is likely that something other than the IME has moved the selection endpoint 796 // to the new value. 797 if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd 798 && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false; 799 // If neither of the above two cases hold, then the system may be having trouble keeping up 800 // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart 801 // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then 802 // assume a belated update. 803 return (newSelStart == newSelEnd) 804 && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0 805 && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0; 806 } 807 808 /** 809 * Looks at the text just before the cursor to find out if it looks like a URL. 810 * 811 * The weakest point here is, if we don't have enough text bufferized, we may fail to realize 812 * we are in URL situation, but other places in this class have the same limitation and it 813 * does not matter too much in the practice. 814 */ 815 public boolean textBeforeCursorLooksLikeURL() { 816 return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); 817 } 818 819 /** 820 * Looks at the text just before the cursor to find out if we are inside a double quote. 821 * 822 * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. 823 * However this won't be a concrete problem in most situations, as the cache is almost always 824 * long enough for this use. 825 */ 826 public boolean isInsideDoubleQuoteOrAfterDigit() { 827 return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); 828 } 829 830 /** 831 * Try to get the text from the editor to expose lies the framework may have been 832 * telling us. Concretely, when the device rotates, the frameworks tells us about where the 833 * cursor used to be initially in the editor at the time it first received the focus; this 834 * may be completely different from the place it is upon rotation. Since we don't have any 835 * means to get the real value, try at least to ask the text view for some characters and 836 * detect the most damaging cases: when the cursor position is declared to be much smaller 837 * than it really is. 838 */ 839 public void tryFixLyingCursorPosition() { 840 final CharSequence textBeforeCursor = getTextBeforeCursor( 841 Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); 842 if (null == textBeforeCursor) { 843 mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION; 844 } else { 845 final int textLength = textBeforeCursor.length(); 846 if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE 847 && (textLength > mExpectedSelStart 848 || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { 849 // It should not be possible to have only one of those variables be 850 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized 851 // (simple cursor, no selection) or there is no cursor/we don't know its pos 852 final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd; 853 mExpectedSelStart = textLength; 854 // We can't figure out the value of mLastSelectionEnd :( 855 // But at least if it's smaller than mLastSelectionStart something is wrong, 856 // and if they used to be equal we also don't want to make it look like there is a 857 // selection. 858 if (wasEqual || mExpectedSelStart > mExpectedSelEnd) { 859 mExpectedSelEnd = mExpectedSelStart; 860 } 861 } 862 } 863 } 864 865 public int getExpectedSelectionStart() { 866 return mExpectedSelStart; 867 } 868 869 public int getExpectedSelectionEnd() { 870 return mExpectedSelEnd; 871 } 872 873 /** 874 * @return whether there is a selection currently active. 875 */ 876 public boolean hasSelection() { 877 return mExpectedSelEnd != mExpectedSelStart; 878 } 879 880 public boolean isCursorPositionKnown() { 881 return INVALID_CURSOR_POSITION != mExpectedSelStart; 882 } 883} 884