RichInputConnection.java revision 7eef5d3ff4a0456335943e6a7494f540a7291017
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 * @param sortedSeparators a sorted array of code points which may separate words 624 * @return the word that surrounds the cursor, including up to one trailing 625 * separator. For example, if the field contains "he|llo world", where | 626 * represents the cursor, then "hello " will be returned. 627 */ 628 public CharSequence getWordAtCursor(final int[] sortedSeparators) { 629 // getWordRangeAtCursor returns null if the connection is null 630 final TextRange r = getWordRangeAtCursor(sortedSeparators, 0); 631 return (r == null) ? null : r.mWord; 632 } 633 634 /** 635 * Returns the text surrounding the cursor. 636 * 637 * @param sortedSeparators a sorted array of code points that split words. 638 * @param additionalPrecedingWordsCount the number of words before the current word that should 639 * be included in the returned range 640 * @return a range containing the text surrounding the cursor 641 */ 642 public TextRange getWordRangeAtCursor(final int[] sortedSeparators, 643 final int additionalPrecedingWordsCount) { 644 mIC = mParent.getCurrentInputConnection(); 645 if (mIC == null) { 646 return null; 647 } 648 final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 649 InputConnection.GET_TEXT_WITH_STYLES); 650 final CharSequence after = mIC.getTextAfterCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 651 InputConnection.GET_TEXT_WITH_STYLES); 652 if (before == null || after == null) { 653 return null; 654 } 655 656 // Going backward, alternate skipping non-separators and separators until enough words 657 // have been read. 658 int count = additionalPrecedingWordsCount; 659 int startIndexInBefore = before.length(); 660 boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at 661 while (true) { // see comments below for why this is guaranteed to halt 662 while (startIndexInBefore > 0) { 663 final int codePoint = Character.codePointBefore(before, startIndexInBefore); 664 if (isStoppingAtWhitespace == isSeparator(codePoint, sortedSeparators)) { 665 break; // inner loop 666 } 667 --startIndexInBefore; 668 if (Character.isSupplementaryCodePoint(codePoint)) { 669 --startIndexInBefore; 670 } 671 } 672 // isStoppingAtWhitespace is true every other time through the loop, 673 // so additionalPrecedingWordsCount is guaranteed to become < 0, which 674 // guarantees outer loop termination 675 if (isStoppingAtWhitespace && (--count < 0)) { 676 break; // outer loop 677 } 678 isStoppingAtWhitespace = !isStoppingAtWhitespace; 679 } 680 681 // Find last word separator after the cursor 682 int endIndexInAfter = -1; 683 while (++endIndexInAfter < after.length()) { 684 final int codePoint = Character.codePointAt(after, endIndexInAfter); 685 if (isSeparator(codePoint, sortedSeparators)) { 686 break; 687 } 688 if (Character.isSupplementaryCodePoint(codePoint)) { 689 ++endIndexInAfter; 690 } 691 } 692 693 final boolean hasUrlSpans = 694 SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length()) 695 || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter); 696 // We don't use TextUtils#concat because it copies all spans without respect to their 697 // nature. If the text includes a PARAGRAPH span and it has been split, then 698 // TextUtils#concat will crash when it tries to concat both sides of it. 699 return new TextRange( 700 SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after), 701 startIndexInBefore, before.length() + endIndexInAfter, before.length(), 702 hasUrlSpans); 703 } 704 705 public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { 706 if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { 707 // If what's after the cursor is a word character, then we're touching a word. 708 return true; 709 } 710 final String textBeforeCursor = mCommittedTextBeforeComposingText.toString(); 711 int indexOfCodePointInJavaChars = textBeforeCursor.length(); 712 int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE 713 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); 714 // Search for the first non word-connector char 715 if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) { 716 indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint); 717 consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE 718 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); 719 } 720 return !(Constants.NOT_A_CODE == consideredCodePoint 721 || spacingAndPunctuations.isWordSeparator(consideredCodePoint) 722 || spacingAndPunctuations.isWordConnector(consideredCodePoint)); 723 } 724 725 public boolean isCursorFollowedByWordCharacter( 726 final SpacingAndPunctuations spacingAndPunctuations) { 727 final CharSequence after = getTextAfterCursor(1, 0); 728 if (TextUtils.isEmpty(after)) { 729 return false; 730 } 731 final int codePointAfterCursor = Character.codePointAt(after, 0); 732 if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor) 733 || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) { 734 return false; 735 } 736 return true; 737 } 738 739 public void removeTrailingSpace() { 740 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 741 final int codePointBeforeCursor = getCodePointBeforeCursor(); 742 if (Constants.CODE_SPACE == codePointBeforeCursor) { 743 deleteSurroundingText(1, 0); 744 } 745 } 746 747 public boolean sameAsTextBeforeCursor(final CharSequence text) { 748 final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); 749 return TextUtils.equals(text, beforeText); 750 } 751 752 public boolean revertDoubleSpacePeriod() { 753 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 754 // Here we test whether we indeed have a period and a space before us. This should not 755 // be needed, but it's there just in case something went wrong. 756 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 757 if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) { 758 // Theoretically we should not be coming here if there isn't ". " before the 759 // cursor, but the application may be changing the text while we are typing, so 760 // anything goes. We should not crash. 761 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 762 + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor."); 763 return false; 764 } 765 // Double-space results in ". ". A backspace to cancel this should result in a single 766 // space in the text field, so we replace ". " with a single space. 767 deleteSurroundingText(2, 0); 768 final String singleSpace = " "; 769 commitText(singleSpace, 1); 770 return true; 771 } 772 773 public boolean revertSwapPunctuation() { 774 if (DEBUG_BATCH_NESTING) checkBatchEdit(); 775 // Here we test whether we indeed have a space and something else before us. This should not 776 // be needed, but it's there just in case something went wrong. 777 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 778 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 779 // enter surrogate pairs this code will have been removed. 780 if (TextUtils.isEmpty(textBeforeCursor) 781 || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) { 782 // We may only come here if the application is changing the text while we are typing. 783 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 784 // but some debugging log may be in order. 785 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 786 + "find a space just before the cursor."); 787 return false; 788 } 789 deleteSurroundingText(2, 0); 790 final String text = " " + textBeforeCursor.subSequence(0, 1); 791 commitText(text, 1); 792 return true; 793 } 794 795 /** 796 * Heuristic to determine if this is an expected update of the cursor. 797 * 798 * Sometimes updates to the cursor position are late because of their asynchronous nature. 799 * This method tries to determine if this update is one, based on the values of the cursor 800 * position in the update, and the currently expected position of the cursor according to 801 * LatinIME's internal accounting. If this is not a belated expected update, then it should 802 * mean that the user moved the cursor explicitly. 803 * This is quite robust, but of course it's not perfect. In particular, it will fail in the 804 * case we get an update A, the user types in N characters so as to move the cursor to A+N but 805 * we don't get those, and then the user places the cursor between A and A+N, and we get only 806 * this update and not the ones in-between. This is almost impossible to achieve even trying 807 * very very hard. 808 * 809 * @param oldSelStart The value of the old selection in the update. 810 * @param newSelStart The value of the new selection in the update. 811 * @param oldSelEnd The value of the old selection end in the update. 812 * @param newSelEnd The value of the new selection end in the update. 813 * @return whether this is a belated expected update or not. 814 */ 815 public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart, 816 final int oldSelEnd, final int newSelEnd) { 817 // This update is "belated" if we are expecting it. That is, mExpectedSelStart and 818 // mExpectedSelEnd match the new values that the TextView is updating TO. 819 if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true; 820 // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old 821 // values, and one of newSelStart or newSelEnd is updated to a different value. In this 822 // case, it is likely that something other than the IME has moved the selection endpoint 823 // to the new value. 824 if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd 825 && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false; 826 // If neither of the above two cases hold, then the system may be having trouble keeping up 827 // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart 828 // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then 829 // assume a belated update. 830 return (newSelStart == newSelEnd) 831 && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0 832 && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0; 833 } 834 835 /** 836 * Looks at the text just before the cursor to find out if it looks like a URL. 837 * 838 * The weakest point here is, if we don't have enough text bufferized, we may fail to realize 839 * we are in URL situation, but other places in this class have the same limitation and it 840 * does not matter too much in the practice. 841 */ 842 public boolean textBeforeCursorLooksLikeURL() { 843 return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); 844 } 845 846 /** 847 * Looks at the text just before the cursor to find out if we are inside a double quote. 848 * 849 * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. 850 * However this won't be a concrete problem in most situations, as the cache is almost always 851 * long enough for this use. 852 */ 853 public boolean isInsideDoubleQuoteOrAfterDigit() { 854 return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); 855 } 856 857 /** 858 * Try to get the text from the editor to expose lies the framework may have been 859 * telling us. Concretely, when the device rotates, the frameworks tells us about where the 860 * cursor used to be initially in the editor at the time it first received the focus; this 861 * may be completely different from the place it is upon rotation. Since we don't have any 862 * means to get the real value, try at least to ask the text view for some characters and 863 * detect the most damaging cases: when the cursor position is declared to be much smaller 864 * than it really is. 865 */ 866 public void tryFixLyingCursorPosition() { 867 final CharSequence textBeforeCursor = getTextBeforeCursor( 868 Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); 869 if (null == textBeforeCursor) { 870 mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION; 871 } else { 872 final int textLength = textBeforeCursor.length(); 873 if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE 874 && (textLength > mExpectedSelStart 875 || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { 876 // It should not be possible to have only one of those variables be 877 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized 878 // (simple cursor, no selection) or there is no cursor/we don't know its pos 879 final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd; 880 mExpectedSelStart = textLength; 881 // We can't figure out the value of mLastSelectionEnd :( 882 // But at least if it's smaller than mLastSelectionStart something is wrong, 883 // and if they used to be equal we also don't want to make it look like there is a 884 // selection. 885 if (wasEqual || mExpectedSelStart > mExpectedSelEnd) { 886 mExpectedSelEnd = mExpectedSelStart; 887 } 888 } 889 } 890 } 891 892 public int getExpectedSelectionStart() { 893 return mExpectedSelStart; 894 } 895 896 public int getExpectedSelectionEnd() { 897 return mExpectedSelEnd; 898 } 899 900 /** 901 * @return whether there is a selection currently active. 902 */ 903 public boolean hasSelection() { 904 return mExpectedSelEnd != mExpectedSelStart; 905 } 906 907 public boolean isCursorPositionKnown() { 908 return INVALID_CURSOR_POSITION != mExpectedSelStart; 909 } 910} 911