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