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