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