RichInputConnection.java revision d579f1aefc8d02254db297ffd6d8f9dbdcab0637
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * 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.keyboard.Keyboard; 30import com.android.inputmethod.latin.define.ProductionFlag; 31 32import java.util.regex.Pattern; 33 34/** 35 * Wrapper for InputConnection to simplify interaction 36 */ 37public class RichInputConnection { 38 private static final String TAG = RichInputConnection.class.getSimpleName(); 39 private static final boolean DBG = false; 40 // Provision for a long word pair and a separator 41 private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1; 42 private static final Pattern spaceRegex = Pattern.compile("\\s+"); 43 private static final int INVALID_CURSOR_POSITION = -1; 44 45 private final InputMethodService mParent; 46 InputConnection mIC; 47 int mNestLevel; 48 public RichInputConnection(final InputMethodService parent) { 49 mParent = parent; 50 mIC = null; 51 mNestLevel = 0; 52 } 53 54 public void beginBatchEdit() { 55 if (++mNestLevel == 1) { 56 mIC = mParent.getCurrentInputConnection(); 57 if (null != mIC) mIC.beginBatchEdit(); 58 } else { 59 if (DBG) { 60 throw new RuntimeException("Nest level too deep"); 61 } else { 62 Log.e(TAG, "Nest level too deep : " + mNestLevel); 63 } 64 } 65 } 66 public void endBatchEdit() { 67 if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead 68 if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit(); 69 } 70 71 private void checkBatchEdit() { 72 if (mNestLevel != 1) { 73 // TODO: exception instead 74 Log.e(TAG, "Batch edit level incorrect : " + mNestLevel); 75 Log.e(TAG, Utils.getStackTrace(4)); 76 } 77 } 78 79 public void finishComposingText() { 80 checkBatchEdit(); 81 if (null != mIC) mIC.finishComposingText(); 82 } 83 84 public void commitText(final CharSequence text, final int i) { 85 checkBatchEdit(); 86 if (null != mIC) mIC.commitText(text, i); 87 } 88 89 public int getCursorCapsMode(final int inputType) { 90 mIC = mParent.getCurrentInputConnection(); 91 if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; 92 return mIC.getCursorCapsMode(inputType); 93 } 94 95 public CharSequence getTextBeforeCursor(final int i, final int j) { 96 mIC = mParent.getCurrentInputConnection(); 97 if (null != mIC) return mIC.getTextBeforeCursor(i, j); 98 return null; 99 } 100 101 public CharSequence getTextAfterCursor(final int i, final int j) { 102 mIC = mParent.getCurrentInputConnection(); 103 if (null != mIC) return mIC.getTextAfterCursor(i, j); 104 return null; 105 } 106 107 public void deleteSurroundingText(final int i, final int j) { 108 checkBatchEdit(); 109 if (null != mIC) mIC.deleteSurroundingText(i, j); 110 } 111 112 public void performEditorAction(final int actionId) { 113 mIC = mParent.getCurrentInputConnection(); 114 if (null != mIC) mIC.performEditorAction(actionId); 115 } 116 117 public void sendKeyEvent(final KeyEvent keyEvent) { 118 checkBatchEdit(); 119 if (null != mIC) mIC.sendKeyEvent(keyEvent); 120 } 121 122 public void setComposingText(final CharSequence text, final int i) { 123 checkBatchEdit(); 124 if (null != mIC) mIC.setComposingText(text, i); 125 } 126 127 public void setSelection(final int from, final int to) { 128 checkBatchEdit(); 129 if (null != mIC) mIC.setSelection(from, to); 130 } 131 132 public void commitCorrection(final CorrectionInfo correctionInfo) { 133 checkBatchEdit(); 134 if (null != mIC) mIC.commitCorrection(correctionInfo); 135 } 136 137 public void commitCompletion(final CompletionInfo completionInfo) { 138 checkBatchEdit(); 139 if (null != mIC) mIC.commitCompletion(completionInfo); 140 } 141 142 public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) { 143 mIC = mParent.getCurrentInputConnection(); 144 if (null == mIC) return null; 145 final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); 146 return getNthPreviousWord(prev, sentenceSeperators, n); 147 } 148 149 /** 150 * Represents a range of text, relative to the current cursor position. 151 */ 152 public static class Range { 153 /** Characters before selection start */ 154 public final int mCharsBefore; 155 156 /** 157 * Characters after selection start, including one trailing word 158 * separator. 159 */ 160 public final int mCharsAfter; 161 162 /** The actual characters that make up a word */ 163 public final String mWord; 164 165 public Range(int charsBefore, int charsAfter, String word) { 166 if (charsBefore < 0 || charsAfter < 0) { 167 throw new IndexOutOfBoundsException(); 168 } 169 this.mCharsBefore = charsBefore; 170 this.mCharsAfter = charsAfter; 171 this.mWord = word; 172 } 173 } 174 175 private static boolean isSeparator(int code, String sep) { 176 return sep.indexOf(code) != -1; 177 } 178 179 // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor, 180 // n = 2 retrieves the word before that, and so on. This splits on whitespace only. 181 // Also, it won't return words that end in a separator (if the nth word before the cursor 182 // ends in a separator, it returns null). 183 // Example : 184 // (n = 1) "abc def|" -> def 185 // (n = 1) "abc def |" -> def 186 // (n = 1) "abc def. |" -> null 187 // (n = 1) "abc def . |" -> null 188 // (n = 2) "abc def|" -> abc 189 // (n = 2) "abc def |" -> abc 190 // (n = 2) "abc def. |" -> abc 191 // (n = 2) "abc def . |" -> def 192 // (n = 2) "abc|" -> null 193 // (n = 2) "abc |" -> null 194 // (n = 2) "abc. def|" -> null 195 public static CharSequence getNthPreviousWord(final CharSequence prev, 196 final String sentenceSeperators, final int n) { 197 if (prev == null) return null; 198 String[] w = spaceRegex.split(prev); 199 200 // If we can't find n words, or we found an empty word, return null. 201 if (w.length < n || w[w.length - n].length() <= 0) return null; 202 203 // If ends in a separator, return null 204 char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1); 205 if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; 206 207 return w[w.length - n]; 208 } 209 210 /** 211 * @param separators characters which may separate words 212 * @return the word that surrounds the cursor, including up to one trailing 213 * separator. For example, if the field contains "he|llo world", where | 214 * represents the cursor, then "hello " will be returned. 215 */ 216 public String getWordAtCursor(String separators) { 217 // getWordRangeAtCursor returns null if the connection is null 218 Range r = getWordRangeAtCursor(separators, 0); 219 return (r == null) ? null : r.mWord; 220 } 221 222 private int getCursorPosition() { 223 mIC = mParent.getCurrentInputConnection(); 224 if (null == mIC) return INVALID_CURSOR_POSITION; 225 final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0); 226 if (extracted == null) { 227 return INVALID_CURSOR_POSITION; 228 } 229 return extracted.startOffset + extracted.selectionStart; 230 } 231 232 /** 233 * Returns the text surrounding the cursor. 234 * 235 * @param sep a string of characters that split words. 236 * @param additionalPrecedingWordsCount the number of words before the current word that should 237 * be included in the returned range 238 * @return a range containing the text surrounding the cursor 239 */ 240 public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) { 241 mIC = mParent.getCurrentInputConnection(); 242 if (mIC == null || sep == null) { 243 return null; 244 } 245 CharSequence before = mIC.getTextBeforeCursor(1000, 0); 246 CharSequence after = mIC.getTextAfterCursor(1000, 0); 247 if (before == null || after == null) { 248 return null; 249 } 250 251 // Going backward, alternate skipping non-separators and separators until enough words 252 // have been read. 253 int start = before.length(); 254 boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at 255 while (true) { // see comments below for why this is guaranteed to halt 256 while (start > 0) { 257 final int codePoint = Character.codePointBefore(before, start); 258 if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) { 259 break; // inner loop 260 } 261 --start; 262 if (Character.isSupplementaryCodePoint(codePoint)) { 263 --start; 264 } 265 } 266 // isStoppingAtWhitespace is true every other time through the loop, 267 // so additionalPrecedingWordsCount is guaranteed to become < 0, which 268 // guarantees outer loop termination 269 if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) { 270 break; // outer loop 271 } 272 isStoppingAtWhitespace = !isStoppingAtWhitespace; 273 } 274 275 // Find last word separator after the cursor 276 int end = -1; 277 while (++end < after.length()) { 278 final int codePoint = Character.codePointAt(after, end); 279 if (isSeparator(codePoint, sep)) { 280 break; 281 } 282 if (Character.isSupplementaryCodePoint(codePoint)) { 283 ++end; 284 } 285 } 286 287 int cursor = getCursorPosition(); 288 if (start >= 0 && cursor + end <= after.length() + before.length()) { 289 String word = before.toString().substring(start, before.length()) 290 + after.toString().substring(0, end); 291 return new Range(before.length() - start, end, word); 292 } 293 294 return null; 295 } 296 297 public boolean isCursorTouchingWord(final SettingsValues settingsValues) { 298 CharSequence before = getTextBeforeCursor(1, 0); 299 CharSequence after = getTextAfterCursor(1, 0); 300 if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0)) 301 && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) { 302 return true; 303 } 304 if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0)) 305 && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) { 306 return true; 307 } 308 return false; 309 } 310 311 public void removeTrailingSpace() { 312 checkBatchEdit(); 313 final CharSequence lastOne = getTextBeforeCursor(1, 0); 314 if (lastOne != null && lastOne.length() == 1 315 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { 316 deleteSurroundingText(1, 0); 317 if (ProductionFlag.IS_EXPERIMENTAL) { 318 ResearchLogger.latinIME_deleteSurroundingText(1); 319 } 320 } 321 } 322 323 public boolean sameAsTextBeforeCursor(final CharSequence text) { 324 final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); 325 return TextUtils.equals(text, beforeText); 326 } 327 328 /* (non-javadoc) 329 * Returns the word before the cursor if the cursor is at the end of a word, null otherwise 330 */ 331 public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) { 332 // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, 333 // separator or end of line/text) 334 // Example: "test|"<EOL> "te|st" get rejected here 335 final CharSequence textAfterCursor = getTextAfterCursor(1, 0); 336 if (!TextUtils.isEmpty(textAfterCursor) 337 && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null; 338 339 // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) 340 // Example: " -|" gets rejected here but "e-|" and "e|" are okay 341 CharSequence word = getWordAtCursor(settings.mWordSeparators); 342 // We don't suggest on leading single quotes, so we have to remove them from the word if 343 // it starts with single quotes. 344 while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) { 345 word = word.subSequence(1, word.length()); 346 } 347 if (TextUtils.isEmpty(word)) return null; 348 // Find the last code point of the string 349 final int lastCodePoint = Character.codePointBefore(word, word.length()); 350 // If for some reason the text field contains non-unicode binary data, or if the 351 // charsequence is exactly one char long and the contents is a low surrogate, return null. 352 if (!Character.isDefined(lastCodePoint)) return null; 353 // Bail out if the cursor is not at the end of a word (cursor must be preceded by 354 // non-whitespace, non-separator, non-start-of-text) 355 // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. 356 if (settings.isWordSeparator(lastCodePoint)) return null; 357 final char firstChar = word.charAt(0); // we just tested that word is not empty 358 if (word.length() == 1 && !Character.isLetter(firstChar)) return null; 359 360 // We only suggest on words that start with a letter or a symbol that is excluded from 361 // word separators (see #handleCharacterWhileInBatchEdit). 362 if (!(Character.isLetter(firstChar) 363 || settings.isSymbolExcludedFromWordSeparators(firstChar))) { 364 return null; 365 } 366 367 return word; 368 } 369 370 public boolean revertDoubleSpace() { 371 checkBatchEdit(); 372 // Here we test whether we indeed have a period and a space before us. This should not 373 // be needed, but it's there just in case something went wrong. 374 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 375 if (!". ".equals(textBeforeCursor)) { 376 // Theoretically we should not be coming here if there isn't ". " before the 377 // cursor, but the application may be changing the text while we are typing, so 378 // anything goes. We should not crash. 379 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 380 + "\". \" just before the cursor."); 381 return false; 382 } 383 deleteSurroundingText(2, 0); 384 if (ProductionFlag.IS_EXPERIMENTAL) { 385 ResearchLogger.latinIME_deleteSurroundingText(2); 386 } 387 commitText(" ", 1); 388 if (ProductionFlag.IS_EXPERIMENTAL) { 389 ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit(); 390 } 391 return true; 392 } 393 394 public boolean revertSwapPunctuation() { 395 checkBatchEdit(); 396 // Here we test whether we indeed have a space and something else before us. This should not 397 // be needed, but it's there just in case something went wrong. 398 final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); 399 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 400 // enter surrogate pairs this code will have been removed. 401 if (TextUtils.isEmpty(textBeforeCursor) 402 || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) { 403 // We may only come here if the application is changing the text while we are typing. 404 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 405 // but some debugging log may be in order. 406 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 407 + "find a space just before the cursor."); 408 return false; 409 } 410 deleteSurroundingText(2, 0); 411 if (ProductionFlag.IS_EXPERIMENTAL) { 412 ResearchLogger.latinIME_deleteSurroundingText(2); 413 } 414 commitText(" " + textBeforeCursor.subSequence(0, 1), 1); 415 if (ProductionFlag.IS_EXPERIMENTAL) { 416 ResearchLogger.latinIME_revertSwapPunctuation(); 417 } 418 return true; 419 } 420} 421