BaseInputConnection.java revision 3dec7d563a2f3e1eb967ce2054a00b6620e3558c
1/* 2 * Copyright (C) 2008 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 android.view.inputmethod; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.os.Bundle; 22import android.os.Handler; 23import android.os.SystemClock; 24import android.text.Editable; 25import android.text.NoCopySpan; 26import android.text.Selection; 27import android.text.Spannable; 28import android.text.SpannableStringBuilder; 29import android.text.Spanned; 30import android.text.TextUtils; 31import android.text.method.MetaKeyKeyListener; 32import android.util.Log; 33import android.util.LogPrinter; 34import android.view.KeyCharacterMap; 35import android.view.KeyEvent; 36import android.view.View; 37import android.view.ViewRoot; 38 39class ComposingText implements NoCopySpan { 40} 41 42/** 43 * Base class for implementors of the InputConnection interface, taking care 44 * of most of the common behavior for providing a connection to an Editable. 45 * Implementors of this class will want to be sure to implement 46 * {@link #getEditable} to provide access to their own editable object. 47 */ 48public class BaseInputConnection implements InputConnection { 49 private static final boolean DEBUG = false; 50 private static final String TAG = "BaseInputConnection"; 51 static final Object COMPOSING = new ComposingText(); 52 53 final InputMethodManager mIMM; 54 final Handler mH; 55 final View mTargetView; 56 final boolean mDummyMode; 57 58 private Object[] mDefaultComposingSpans; 59 60 Editable mEditable; 61 KeyCharacterMap mKeyCharacterMap; 62 63 BaseInputConnection(InputMethodManager mgr, boolean dummyMode) { 64 mIMM = mgr; 65 mTargetView = null; 66 mH = null; 67 mDummyMode = dummyMode; 68 } 69 70 public BaseInputConnection(View targetView, boolean dummyMode) { 71 mIMM = (InputMethodManager)targetView.getContext().getSystemService( 72 Context.INPUT_METHOD_SERVICE); 73 mH = targetView.getHandler(); 74 mTargetView = targetView; 75 mDummyMode = dummyMode; 76 } 77 78 public static final void removeComposingSpans(Spannable text) { 79 text.removeSpan(COMPOSING); 80 Object[] sps = text.getSpans(0, text.length(), Object.class); 81 if (sps != null) { 82 for (int i=sps.length-1; i>=0; i--) { 83 Object o = sps[i]; 84 if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) { 85 text.removeSpan(o); 86 } 87 } 88 } 89 } 90 91 public static void setComposingSpans(Spannable text) { 92 final Object[] sps = text.getSpans(0, text.length(), Object.class); 93 if (sps != null) { 94 for (int i=sps.length-1; i>=0; i--) { 95 final Object o = sps[i]; 96 if (o == COMPOSING) { 97 text.removeSpan(o); 98 continue; 99 } 100 final int fl = text.getSpanFlags(o); 101 if ((fl&(Spanned.SPAN_COMPOSING|Spanned.SPAN_POINT_MARK_MASK)) 102 != (Spanned.SPAN_COMPOSING|Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) { 103 text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o), 104 (fl&Spanned.SPAN_POINT_MARK_MASK) 105 | Spanned.SPAN_COMPOSING 106 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 107 } 108 } 109 } 110 111 text.setSpan(COMPOSING, 0, text.length(), 112 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); 113 } 114 115 public static int getComposingSpanStart(Spannable text) { 116 return text.getSpanStart(COMPOSING); 117 } 118 119 public static int getComposingSpanEnd(Spannable text) { 120 return text.getSpanEnd(COMPOSING); 121 } 122 123 /** 124 * Return the target of edit operations. The default implementation 125 * returns its own fake editable that is just used for composing text; 126 * subclasses that are real text editors should override this and 127 * supply their own. 128 */ 129 public Editable getEditable() { 130 if (mEditable == null) { 131 mEditable = Editable.Factory.getInstance().newEditable(""); 132 Selection.setSelection(mEditable, 0); 133 } 134 return mEditable; 135 } 136 137 /** 138 * Default implementation does nothing. 139 */ 140 public boolean beginBatchEdit() { 141 return false; 142 } 143 144 /** 145 * Default implementation does nothing. 146 */ 147 public boolean endBatchEdit() { 148 return false; 149 } 150 151 /** 152 * Default implementation uses 153 * {@link MetaKeyKeyListener#clearMetaKeyState(long, int) 154 * MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state. 155 */ 156 public boolean clearMetaKeyStates(int states) { 157 final Editable content = getEditable(); 158 if (content == null) return false; 159 MetaKeyKeyListener.clearMetaKeyState(content, states); 160 return true; 161 } 162 163 /** 164 * Default implementation does nothing. 165 */ 166 public boolean commitCompletion(CompletionInfo text) { 167 return false; 168 } 169 170 /** 171 * Default implementation replaces any existing composing text with 172 * the given text. In addition, only if dummy mode, a key event is 173 * sent for the new text and the current editable buffer cleared. 174 */ 175 public boolean commitText(CharSequence text, int newCursorPosition) { 176 if (DEBUG) Log.v(TAG, "commitText " + text); 177 replaceText(text, newCursorPosition, false); 178 sendCurrentText(); 179 return true; 180 } 181 182 /** 183 * The default implementation performs the deletion around the current 184 * selection position of the editable text. 185 */ 186 public boolean deleteSurroundingText(int leftLength, int rightLength) { 187 if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength 188 + " / " + rightLength); 189 final Editable content = getEditable(); 190 if (content == null) return false; 191 192 beginBatchEdit(); 193 194 int a = Selection.getSelectionStart(content); 195 int b = Selection.getSelectionEnd(content); 196 197 if (a > b) { 198 int tmp = a; 199 a = b; 200 b = tmp; 201 } 202 203 // ignore the composing text. 204 int ca = getComposingSpanStart(content); 205 int cb = getComposingSpanEnd(content); 206 if (cb < ca) { 207 int tmp = ca; 208 ca = cb; 209 cb = tmp; 210 } 211 if (ca != -1 && cb != -1) { 212 if (ca < a) a = ca; 213 if (cb > b) b = cb; 214 } 215 216 int deleted = 0; 217 218 if (leftLength > 0) { 219 int start = a - leftLength; 220 if (start < 0) start = 0; 221 content.delete(start, a); 222 deleted = a - start; 223 } 224 225 if (rightLength > 0) { 226 b = b - deleted; 227 228 int end = b + rightLength; 229 if (end > content.length()) end = content.length(); 230 231 content.delete(b, end); 232 } 233 234 endBatchEdit(); 235 236 return true; 237 } 238 239 /** 240 * The default implementation removes the composing state from the 241 * current editable text. In addition, only if dummy mode, a key event is 242 * sent for the new text and the current editable buffer cleared. 243 */ 244 public boolean finishComposingText() { 245 if (DEBUG) Log.v(TAG, "finishComposingText"); 246 final Editable content = getEditable(); 247 if (content != null) { 248 beginBatchEdit(); 249 removeComposingSpans(content); 250 endBatchEdit(); 251 sendCurrentText(); 252 } 253 return true; 254 } 255 256 /** 257 * The default implementation uses TextUtils.getCapsMode to get the 258 * cursor caps mode for the current selection position in the editable 259 * text, unless in dummy mode in which case 0 is always returned. 260 */ 261 public int getCursorCapsMode(int reqModes) { 262 if (mDummyMode) return 0; 263 264 final Editable content = getEditable(); 265 if (content == null) return 0; 266 267 int a = Selection.getSelectionStart(content); 268 int b = Selection.getSelectionEnd(content); 269 270 if (a > b) { 271 int tmp = a; 272 a = b; 273 b = tmp; 274 } 275 276 return TextUtils.getCapsMode(content, a, reqModes); 277 } 278 279 /** 280 * The default implementation always returns null. 281 */ 282 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 283 return null; 284 } 285 286 /** 287 * The default implementation returns the given amount of text from the 288 * current cursor position in the buffer. 289 */ 290 public CharSequence getTextBeforeCursor(int length, int flags) { 291 final Editable content = getEditable(); 292 if (content == null) return null; 293 294 int a = Selection.getSelectionStart(content); 295 int b = Selection.getSelectionEnd(content); 296 297 if (a > b) { 298 int tmp = a; 299 a = b; 300 b = tmp; 301 } 302 303 if (length > a) { 304 length = a; 305 } 306 307 if ((flags&GET_TEXT_WITH_STYLES) != 0) { 308 return content.subSequence(a - length, a); 309 } 310 return TextUtils.substring(content, a - length, a); 311 } 312 313 /** 314 * The default implementation returns the given amount of text from the 315 * current cursor position in the buffer. 316 */ 317 public CharSequence getTextAfterCursor(int length, int flags) { 318 final Editable content = getEditable(); 319 if (content == null) return null; 320 321 int a = Selection.getSelectionStart(content); 322 int b = Selection.getSelectionEnd(content); 323 324 if (a > b) { 325 int tmp = a; 326 a = b; 327 b = tmp; 328 } 329 330 if (b + length > content.length()) { 331 length = content.length() - b; 332 } 333 334 335 if ((flags&GET_TEXT_WITH_STYLES) != 0) { 336 return content.subSequence(b, b + length); 337 } 338 return TextUtils.substring(content, b, b + length); 339 } 340 341 /** 342 * The default implementation does nothing. 343 */ 344 public boolean performEditorAction(int actionCode) { 345 return false; 346 } 347 348 /** 349 * The default implementation does nothing. 350 */ 351 public boolean performContextMenuAction(int id) { 352 return false; 353 } 354 355 /** 356 * The default implementation does nothing. 357 */ 358 public boolean performPrivateCommand(String action, Bundle data) { 359 return false; 360 } 361 362 /** 363 * The default implementation places the given text into the editable, 364 * replacing any existing composing text. The new text is marked as 365 * in a composing state with the composing style. 366 */ 367 public boolean setComposingText(CharSequence text, int newCursorPosition) { 368 if (DEBUG) Log.v(TAG, "setComposingText " + text); 369 replaceText(text, newCursorPosition, true); 370 return true; 371 } 372 373 /** 374 * The default implementation changes the selection position in the 375 * current editable text. 376 */ 377 public boolean setSelection(int start, int end) { 378 if (DEBUG) Log.v(TAG, "setSelection " + start + ", " + end); 379 final Editable content = getEditable(); 380 if (content == null) return false; 381 int len = content.length(); 382 if (start > len || end > len) { 383 // If the given selection is out of bounds, just ignore it. 384 // Most likely the text was changed out from under the IME, 385 // the the IME is going to have to update all of its state 386 // anyway. 387 return true; 388 } 389 Selection.setSelection(content, start, end); 390 return true; 391 } 392 393 /** 394 * Provides standard implementation for sending a key event to the window 395 * attached to the input connection's view. 396 */ 397 public boolean sendKeyEvent(KeyEvent event) { 398 synchronized (mIMM.mH) { 399 Handler h = mH; 400 if (h == null) { 401 if (mIMM.mServedView != null) { 402 h = mIMM.mServedView.getHandler(); 403 } 404 } 405 if (h != null) { 406 h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, 407 event)); 408 } 409 } 410 return false; 411 } 412 413 /** 414 * Updates InputMethodManager with the current fullscreen mode. 415 */ 416 public boolean reportFullscreenMode(boolean enabled) { 417 mIMM.setFullscreenMode(enabled); 418 return true; 419 } 420 421 private void sendCurrentText() { 422 if (!mDummyMode) { 423 return; 424 } 425 426 Editable content = getEditable(); 427 if (content != null) { 428 final int N = content.length(); 429 if (N == 0) { 430 return; 431 } 432 if (N == 1) { 433 // If it's 1 character, we have a chance of being 434 // able to generate normal key events... 435 if (mKeyCharacterMap == null) { 436 mKeyCharacterMap = KeyCharacterMap.load( 437 KeyCharacterMap.BUILT_IN_KEYBOARD); 438 } 439 char[] chars = new char[1]; 440 content.getChars(0, 1, chars, 0); 441 KeyEvent[] events = mKeyCharacterMap.getEvents(chars); 442 if (events != null) { 443 for (int i=0; i<events.length; i++) { 444 if (DEBUG) Log.v(TAG, "Sending: " + events[i]); 445 sendKeyEvent(events[i]); 446 } 447 content.clear(); 448 return; 449 } 450 } 451 452 // Otherwise, revert to the special key event containing 453 // the actual characters. 454 KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(), 455 content.toString(), KeyCharacterMap.BUILT_IN_KEYBOARD, 0); 456 sendKeyEvent(event); 457 content.clear(); 458 } 459 } 460 461 private void replaceText(CharSequence text, int newCursorPosition, 462 boolean composing) { 463 final Editable content = getEditable(); 464 if (content == null) { 465 return; 466 } 467 468 beginBatchEdit(); 469 470 // delete composing text set previously. 471 int a = getComposingSpanStart(content); 472 int b = getComposingSpanEnd(content); 473 474 if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b); 475 476 if (b < a) { 477 int tmp = a; 478 a = b; 479 b = tmp; 480 } 481 482 if (a != -1 && b != -1) { 483 removeComposingSpans(content); 484 } else { 485 a = Selection.getSelectionStart(content); 486 b = Selection.getSelectionEnd(content); 487 if (a >=0 && b>= 0 && a != b) { 488 if (b < a) { 489 int tmp = a; 490 a = b; 491 b = tmp; 492 } 493 } 494 } 495 496 if (composing) { 497 Spannable sp = null; 498 if (!(text instanceof Spannable)) { 499 sp = new SpannableStringBuilder(text); 500 text = sp; 501 if (mDefaultComposingSpans == null) { 502 Context context; 503 if (mTargetView != null) { 504 context = mTargetView.getContext(); 505 } else if (mIMM.mServedView != null) { 506 context = mIMM.mServedView.getContext(); 507 } else { 508 context = null; 509 } 510 if (context != null) { 511 TypedArray ta = context.getTheme() 512 .obtainStyledAttributes(new int[] { 513 com.android.internal.R.attr.candidatesTextStyleSpans 514 }); 515 CharSequence style = ta.getText(0); 516 ta.recycle(); 517 if (style != null && style instanceof Spanned) { 518 mDefaultComposingSpans = ((Spanned)style).getSpans( 519 0, style.length(), Object.class); 520 } 521 } 522 } 523 if (mDefaultComposingSpans != null) { 524 for (int i = 0; i < mDefaultComposingSpans.length; ++i) { 525 sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(), 526 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 527 } 528 } 529 } else { 530 sp = (Spannable)text; 531 } 532 setComposingSpans(sp); 533 } 534 535 if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \"" 536 + text + "\", composing=" + composing 537 + ", type=" + text.getClass().getCanonicalName()); 538 539 if (DEBUG) { 540 LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); 541 lp.println("Current text:"); 542 TextUtils.dumpSpans(content, lp, " "); 543 lp.println("Composing text:"); 544 TextUtils.dumpSpans(text, lp, " "); 545 } 546 547 // Position the cursor appropriately, so that after replacing the 548 // desired range of text it will be located in the correct spot. 549 // This allows us to deal with filters performing edits on the text 550 // we are providing here. 551 if (newCursorPosition > 0) { 552 newCursorPosition += b - 1; 553 } else { 554 newCursorPosition += a; 555 } 556 if (newCursorPosition < 0) newCursorPosition = 0; 557 if (newCursorPosition > content.length()) 558 newCursorPosition = content.length(); 559 Selection.setSelection(content, newCursorPosition); 560 561 content.replace(a, b, text); 562 563 if (DEBUG) { 564 LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); 565 lp.println("Final text:"); 566 TextUtils.dumpSpans(content, lp, " "); 567 } 568 569 endBatchEdit(); 570 } 571} 572