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