1 /* 2 * Copyright (C) 2008-2009 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.example.android.softkeyboard; 18 19 import android.inputmethodservice.InputMethodService; 20 import android.inputmethodservice.Keyboard; 21 import android.inputmethodservice.KeyboardView; 22 import android.text.InputType; 23 import android.text.method.MetaKeyKeyListener; 24 import android.view.KeyCharacterMap; 25 import android.view.KeyEvent; 26 import android.view.View; 27 import android.view.inputmethod.CompletionInfo; 28 import android.view.inputmethod.EditorInfo; 29 import android.view.inputmethod.InputConnection; 30 import android.view.inputmethod.InputMethodManager; 31 import android.view.inputmethod.InputMethodSubtype; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * Example of writing an input method for a soft keyboard. This code is 38 * focused on simplicity over completeness, so it should in no way be considered 39 * to be a complete soft keyboard implementation. Its purpose is to provide 40 * a basic example for how you would get started writing an input method, to 41 * be fleshed out as appropriate. 42 */ 43 public class SoftKeyboard extends InputMethodService 44 implements KeyboardView.OnKeyboardActionListener { 45 static final boolean DEBUG = false; 46 47 /** 48 * This boolean indicates the optional example code for performing 49 * processing of hard keys in addition to regular text generation 50 * from on-screen interaction. It would be used for input methods that 51 * perform language translations (such as converting text entered on 52 * a QWERTY keyboard to Chinese), but may not be used for input methods 53 * that are primarily intended to be used for on-screen text entry. 54 */ 55 static final boolean PROCESS_HARD_KEYS = true; 56 57 private InputMethodManager mInputMethodManager; 58 59 private LatinKeyboardView mInputView; 60 private CandidateView mCandidateView; 61 private CompletionInfo[] mCompletions; 62 63 private StringBuilder mComposing = new StringBuilder(); 64 private boolean mPredictionOn; 65 private boolean mCompletionOn; 66 private int mLastDisplayWidth; 67 private boolean mCapsLock; 68 private long mLastShiftTime; 69 private long mMetaState; 70 71 private LatinKeyboard mSymbolsKeyboard; 72 private LatinKeyboard mSymbolsShiftedKeyboard; 73 private LatinKeyboard mQwertyKeyboard; 74 75 private LatinKeyboard mCurKeyboard; 76 77 private String mWordSeparators; 78 79 /** 80 * Main initialization of the input method component. Be sure to call 81 * to super class. 82 */ 83 @Override public void onCreate() { 84 super.onCreate(); 85 mInputMethodManager = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); 86 mWordSeparators = getResources().getString(R.string.word_separators); 87 } 88 89 /** 90 * This is the point where you can do all of your UI initialization. It 91 * is called after creation and any configuration change. 92 */ 93 @Override public void onInitializeInterface() { 94 if (mQwertyKeyboard != null) { 95 // Configuration changes can happen after the keyboard gets recreated, 96 // so we need to be able to re-build the keyboards if the available 97 // space has changed. 98 int displayWidth = getMaxWidth(); 99 if (displayWidth == mLastDisplayWidth) return; 100 mLastDisplayWidth = displayWidth; 101 } 102 mQwertyKeyboard = new LatinKeyboard(this, R.xml.qwerty); 103 mSymbolsKeyboard = new LatinKeyboard(this, R.xml.symbols); 104 mSymbolsShiftedKeyboard = new LatinKeyboard(this, R.xml.symbols_shift); 105 } 106 107 /** 108 * Called by the framework when your view for creating input needs to 109 * be generated. This will be called the first time your input method 110 * is displayed, and every time it needs to be re-created such as due to 111 * a configuration change. 112 */ 113 @Override public View onCreateInputView() { 114 mInputView = (LatinKeyboardView) getLayoutInflater().inflate( 115 R.layout.input, null); 116 mInputView.setOnKeyboardActionListener(this); 117 mInputView.setKeyboard(mQwertyKeyboard); 118 return mInputView; 119 } 120 121 /** 122 * Called by the framework when your view for showing candidates needs to 123 * be generated, like {@link #onCreateInputView}. 124 */ 125 @Override public View onCreateCandidatesView() { 126 mCandidateView = new CandidateView(this); 127 mCandidateView.setService(this); 128 return mCandidateView; 129 } 130 131 /** 132 * This is the main point where we do our initialization of the input method 133 * to begin operating on an application. At this point we have been 134 * bound to the client, and are now receiving all of the detailed information 135 * about the target of our edits. 136 */ 137 @Override public void onStartInput(EditorInfo attribute, boolean restarting) { 138 super.onStartInput(attribute, restarting); 139 140 // Reset our state. We want to do this even if restarting, because 141 // the underlying state of the text editor could have changed in any way. 142 mComposing.setLength(0); 143 updateCandidates(); 144 145 if (!restarting) { 146 // Clear shift states. 147 mMetaState = 0; 148 } 149 150 mPredictionOn = false; 151 mCompletionOn = false; 152 mCompletions = null; 153 154 // We are now going to initialize our state based on the type of 155 // text being edited. 156 switch (attribute.inputType & InputType.TYPE_MASK_CLASS) { 157 case InputType.TYPE_CLASS_NUMBER: 158 case InputType.TYPE_CLASS_DATETIME: 159 // Numbers and dates default to the symbols keyboard, with 160 // no extra features. 161 mCurKeyboard = mSymbolsKeyboard; 162 break; 163 164 case InputType.TYPE_CLASS_PHONE: 165 // Phones will also default to the symbols keyboard, though 166 // often you will want to have a dedicated phone keyboard. 167 mCurKeyboard = mSymbolsKeyboard; 168 break; 169 170 case InputType.TYPE_CLASS_TEXT: 171 // This is general text editing. We will default to the 172 // normal alphabetic keyboard, and assume that we should 173 // be doing predictive text (showing candidates as the 174 // user types). 175 mCurKeyboard = mQwertyKeyboard; 176 mPredictionOn = true; 177 178 // We now look for a few special variations of text that will 179 // modify our behavior. 180 int variation = attribute.inputType & InputType.TYPE_MASK_VARIATION; 181 if (variation == InputType.TYPE_TEXT_VARIATION_PASSWORD || 182 variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { 183 // Do not display predictions / what the user is typing 184 // when they are entering a password. 185 mPredictionOn = false; 186 } 187 188 if (variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS 189 || variation == InputType.TYPE_TEXT_VARIATION_URI 190 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 191 // Our predictions are not useful for e-mail addresses 192 // or URIs. 193 mPredictionOn = false; 194 } 195 196 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { 197 // If this is an auto-complete text view, then our predictions 198 // will not be shown and instead we will allow the editor 199 // to supply their own. We only show the editor's 200 // candidates when in fullscreen mode, otherwise relying 201 // own it displaying its own UI. 202 mPredictionOn = false; 203 mCompletionOn = isFullscreenMode(); 204 } 205 206 // We also want to look at the current state of the editor 207 // to decide whether our alphabetic keyboard should start out 208 // shifted. 209 updateShiftKeyState(attribute); 210 break; 211 212 default: 213 // For all unknown input types, default to the alphabetic 214 // keyboard with no special features. 215 mCurKeyboard = mQwertyKeyboard; 216 updateShiftKeyState(attribute); 217 } 218 219 // Update the label on the enter key, depending on what the application 220 // says it will do. 221 mCurKeyboard.setImeOptions(getResources(), attribute.imeOptions); 222 } 223 224 /** 225 * This is called when the user is done editing a field. We can use 226 * this to reset our state. 227 */ 228 @Override public void onFinishInput() { 229 super.onFinishInput(); 230 231 // Clear current composing text and candidates. 232 mComposing.setLength(0); 233 updateCandidates(); 234 235 // We only hide the candidates window when finishing input on 236 // a particular editor, to avoid popping the underlying application 237 // up and down if the user is entering text into the bottom of 238 // its window. 239 setCandidatesViewShown(false); 240 241 mCurKeyboard = mQwertyKeyboard; 242 if (mInputView != null) { 243 mInputView.closing(); 244 } 245 } 246 247 @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { 248 super.onStartInputView(attribute, restarting); 249 // Apply the selected keyboard to the input view. 250 mInputView.setKeyboard(mCurKeyboard); 251 mInputView.closing(); 252 final InputMethodSubtype subtype = mInputMethodManager.getCurrentInputMethodSubtype(); 253 mInputView.setSubtypeOnSpaceKey(subtype); 254 } 255 256 @Override 257 public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { 258 mInputView.setSubtypeOnSpaceKey(subtype); 259 } 260 261 /** 262 * Deal with the editor reporting movement of its cursor. 263 */ 264 @Override public void onUpdateSelection(int oldSelStart, int oldSelEnd, 265 int newSelStart, int newSelEnd, 266 int candidatesStart, int candidatesEnd) { 267 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 268 candidatesStart, candidatesEnd); 269 270 // If the current selection in the text view changes, we should 271 // clear whatever candidate text we have. 272 if (mComposing.length() > 0 && (newSelStart != candidatesEnd 273 || newSelEnd != candidatesEnd)) { 274 mComposing.setLength(0); 275 updateCandidates(); 276 InputConnection ic = getCurrentInputConnection(); 277 if (ic != null) { 278 ic.finishComposingText(); 279 } 280 } 281 } 282 283 /** 284 * This tells us about completions that the editor has determined based 285 * on the current text in it. We want to use this in fullscreen mode 286 * to show the completions ourself, since the editor can not be seen 287 * in that situation. 288 */ 289 @Override public void onDisplayCompletions(CompletionInfo[] completions) { 290 if (mCompletionOn) { 291 mCompletions = completions; 292 if (completions == null) { 293 setSuggestions(null, false, false); 294 return; 295 } 296 297 List<String> stringList = new ArrayList<String>(); 298 for (int i = 0; i < completions.length; i++) { 299 CompletionInfo ci = completions[i]; 300 if (ci != null) stringList.add(ci.getText().toString()); 301 } 302 setSuggestions(stringList, true, true); 303 } 304 } 305 306 /** 307 * This translates incoming hard key events in to edit operations on an 308 * InputConnection. It is only needed when using the 309 * PROCESS_HARD_KEYS option. 310 */ 311 private boolean translateKeyDown(int keyCode, KeyEvent event) { 312 mMetaState = MetaKeyKeyListener.handleKeyDown(mMetaState, 313 keyCode, event); 314 int c = event.getUnicodeChar(MetaKeyKeyListener.getMetaState(mMetaState)); 315 mMetaState = MetaKeyKeyListener.adjustMetaAfterKeypress(mMetaState); 316 InputConnection ic = getCurrentInputConnection(); 317 if (c == 0 || ic == null) { 318 return false; 319 } 320 321 boolean dead = false; 322 323 if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) { 324 dead = true; 325 c = c & KeyCharacterMap.COMBINING_ACCENT_MASK; 326 } 327 328 if (mComposing.length() > 0) { 329 char accent = mComposing.charAt(mComposing.length() -1 ); 330 int composed = KeyEvent.getDeadChar(accent, c); 331 332 if (composed != 0) { 333 c = composed; 334 mComposing.setLength(mComposing.length()-1); 335 } 336 } 337 338 onKey(c, null); 339 340 return true; 341 } 342 343 /** 344 * Use this to monitor key events being delivered to the application. 345 * We get first crack at them, and can either resume them or let them 346 * continue to the app. 347 */ 348 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { 349 switch (keyCode) { 350 case KeyEvent.KEYCODE_BACK: 351 // The InputMethodService already takes care of the back 352 // key for us, to dismiss the input method if it is shown. 353 // However, our keyboard could be showing a pop-up window 354 // that back should dismiss, so we first allow it to do that. 355 if (event.getRepeatCount() == 0 && mInputView != null) { 356 if (mInputView.handleBack()) { 357 return true; 358 } 359 } 360 break; 361 362 case KeyEvent.KEYCODE_DEL: 363 // Special handling of the delete key: if we currently are 364 // composing text for the user, we want to modify that instead 365 // of let the application to the delete itself. 366 if (mComposing.length() > 0) { 367 onKey(Keyboard.KEYCODE_DELETE, null); 368 return true; 369 } 370 break; 371 372 case KeyEvent.KEYCODE_ENTER: 373 // Let the underlying text editor always handle these. 374 return false; 375 376 default: 377 // For all other keys, if we want to do transformations on 378 // text being entered with a hard keyboard, we need to process 379 // it and do the appropriate action. 380 if (PROCESS_HARD_KEYS) { 381 if (keyCode == KeyEvent.KEYCODE_SPACE 382 && (event.getMetaState()&KeyEvent.META_ALT_ON) != 0) { 383 // A silly example: in our input method, Alt+Space 384 // is a shortcut for 'android' in lower case. 385 InputConnection ic = getCurrentInputConnection(); 386 if (ic != null) { 387 // First, tell the editor that it is no longer in the 388 // shift state, since we are consuming this. 389 ic.clearMetaKeyStates(KeyEvent.META_ALT_ON); 390 keyDownUp(KeyEvent.KEYCODE_A); 391 keyDownUp(KeyEvent.KEYCODE_N); 392 keyDownUp(KeyEvent.KEYCODE_D); 393 keyDownUp(KeyEvent.KEYCODE_R); 394 keyDownUp(KeyEvent.KEYCODE_O); 395 keyDownUp(KeyEvent.KEYCODE_I); 396 keyDownUp(KeyEvent.KEYCODE_D); 397 // And we consume this event. 398 return true; 399 } 400 } 401 if (mPredictionOn && translateKeyDown(keyCode, event)) { 402 return true; 403 } 404 } 405 } 406 407 return super.onKeyDown(keyCode, event); 408 } 409 410 /** 411 * Use this to monitor key events being delivered to the application. 412 * We get first crack at them, and can either resume them or let them 413 * continue to the app. 414 */ 415 @Override public boolean onKeyUp(int keyCode, KeyEvent event) { 416 // If we want to do transformations on text being entered with a hard 417 // keyboard, we need to process the up events to update the meta key 418 // state we are tracking. 419 if (PROCESS_HARD_KEYS) { 420 if (mPredictionOn) { 421 mMetaState = MetaKeyKeyListener.handleKeyUp(mMetaState, 422 keyCode, event); 423 } 424 } 425 426 return super.onKeyUp(keyCode, event); 427 } 428 429 /** 430 * Helper function to commit any text being composed in to the editor. 431 */ 432 private void commitTyped(InputConnection inputConnection) { 433 if (mComposing.length() > 0) { 434 inputConnection.commitText(mComposing, mComposing.length()); 435 mComposing.setLength(0); 436 updateCandidates(); 437 } 438 } 439 440 /** 441 * Helper to update the shift state of our keyboard based on the initial 442 * editor state. 443 */ 444 private void updateShiftKeyState(EditorInfo attr) { 445 if (attr != null 446 && mInputView != null && mQwertyKeyboard == mInputView.getKeyboard()) { 447 int caps = 0; 448 EditorInfo ei = getCurrentInputEditorInfo(); 449 if (ei != null && ei.inputType != InputType.TYPE_NULL) { 450 caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); 451 } 452 mInputView.setShifted(mCapsLock || caps != 0); 453 } 454 } 455 456 /** 457 * Helper to determine if a given character code is alphabetic. 458 */ 459 private boolean isAlphabet(int code) { 460 if (Character.isLetter(code)) { 461 return true; 462 } else { 463 return false; 464 } 465 } 466 467 /** 468 * Helper to send a key down / key up pair to the current editor. 469 */ 470 private void keyDownUp(int keyEventCode) { 471 getCurrentInputConnection().sendKeyEvent( 472 new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode)); 473 getCurrentInputConnection().sendKeyEvent( 474 new KeyEvent(KeyEvent.ACTION_UP, keyEventCode)); 475 } 476 477 /** 478 * Helper to send a character to the editor as raw key events. 479 */ 480 private void sendKey(int keyCode) { 481 switch (keyCode) { 482 case '\n': 483 keyDownUp(KeyEvent.KEYCODE_ENTER); 484 break; 485 default: 486 if (keyCode >= '0' && keyCode <= '9') { 487 keyDownUp(keyCode - '0' + KeyEvent.KEYCODE_0); 488 } else { 489 getCurrentInputConnection().commitText(String.valueOf((char) keyCode), 1); 490 } 491 break; 492 } 493 } 494 495 // Implementation of KeyboardViewListener 496 497 public void onKey(int primaryCode, int[] keyCodes) { 498 if (isWordSeparator(primaryCode)) { 499 // Handle separator 500 if (mComposing.length() > 0) { 501 commitTyped(getCurrentInputConnection()); 502 } 503 sendKey(primaryCode); 504 updateShiftKeyState(getCurrentInputEditorInfo()); 505 } else if (primaryCode == Keyboard.KEYCODE_DELETE) { 506 handleBackspace(); 507 } else if (primaryCode == Keyboard.KEYCODE_SHIFT) { 508 handleShift(); 509 } else if (primaryCode == Keyboard.KEYCODE_CANCEL) { 510 handleClose(); 511 return; 512 } else if (primaryCode == LatinKeyboardView.KEYCODE_OPTIONS) { 513 // Show a menu or somethin' 514 } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE 515 && mInputView != null) { 516 Keyboard current = mInputView.getKeyboard(); 517 if (current == mSymbolsKeyboard || current == mSymbolsShiftedKeyboard) { 518 current = mQwertyKeyboard; 519 } else { 520 current = mSymbolsKeyboard; 521 } 522 mInputView.setKeyboard(current); 523 if (current == mSymbolsKeyboard) { 524 current.setShifted(false); 525 } 526 } else { 527 handleCharacter(primaryCode, keyCodes); 528 } 529 } 530 531 public void onText(CharSequence text) { 532 InputConnection ic = getCurrentInputConnection(); 533 if (ic == null) return; 534 ic.beginBatchEdit(); 535 if (mComposing.length() > 0) { 536 commitTyped(ic); 537 } 538 ic.commitText(text, 0); 539 ic.endBatchEdit(); 540 updateShiftKeyState(getCurrentInputEditorInfo()); 541 } 542 543 /** 544 * Update the list of available candidates from the current composing 545 * text. This will need to be filled in by however you are determining 546 * candidates. 547 */ 548 private void updateCandidates() { 549 if (!mCompletionOn) { 550 if (mComposing.length() > 0) { 551 ArrayList<String> list = new ArrayList<String>(); 552 list.add(mComposing.toString()); 553 setSuggestions(list, true, true); 554 } else { 555 setSuggestions(null, false, false); 556 } 557 } 558 } 559 560 public void setSuggestions(List<String> suggestions, boolean completions, 561 boolean typedWordValid) { 562 if (suggestions != null && suggestions.size() > 0) { 563 setCandidatesViewShown(true); 564 } else if (isExtractViewShown()) { 565 setCandidatesViewShown(true); 566 } 567 if (mCandidateView != null) { 568 mCandidateView.setSuggestions(suggestions, completions, typedWordValid); 569 } 570 } 571 572 private void handleBackspace() { 573 final int length = mComposing.length(); 574 if (length > 1) { 575 mComposing.delete(length - 1, length); 576 getCurrentInputConnection().setComposingText(mComposing, 1); 577 updateCandidates(); 578 } else if (length > 0) { 579 mComposing.setLength(0); 580 getCurrentInputConnection().commitText("", 0); 581 updateCandidates(); 582 } else { 583 keyDownUp(KeyEvent.KEYCODE_DEL); 584 } 585 updateShiftKeyState(getCurrentInputEditorInfo()); 586 } 587 588 private void handleShift() { 589 if (mInputView == null) { 590 return; 591 } 592 593 Keyboard currentKeyboard = mInputView.getKeyboard(); 594 if (mQwertyKeyboard == currentKeyboard) { 595 // Alphabet keyboard 596 checkToggleCapsLock(); 597 mInputView.setShifted(mCapsLock || !mInputView.isShifted()); 598 } else if (currentKeyboard == mSymbolsKeyboard) { 599 mSymbolsKeyboard.setShifted(true); 600 mInputView.setKeyboard(mSymbolsShiftedKeyboard); 601 mSymbolsShiftedKeyboard.setShifted(true); 602 } else if (currentKeyboard == mSymbolsShiftedKeyboard) { 603 mSymbolsShiftedKeyboard.setShifted(false); 604 mInputView.setKeyboard(mSymbolsKeyboard); 605 mSymbolsKeyboard.setShifted(false); 606 } 607 } 608 609 private void handleCharacter(int primaryCode, int[] keyCodes) { 610 if (isInputViewShown()) { 611 if (mInputView.isShifted()) { 612 primaryCode = Character.toUpperCase(primaryCode); 613 } 614 } 615 if (isAlphabet(primaryCode) && mPredictionOn) { 616 mComposing.append((char) primaryCode); 617 getCurrentInputConnection().setComposingText(mComposing, 1); 618 updateShiftKeyState(getCurrentInputEditorInfo()); 619 updateCandidates(); 620 } else { 621 getCurrentInputConnection().commitText( 622 String.valueOf((char) primaryCode), 1); 623 } 624 } 625 626 private void handleClose() { 627 commitTyped(getCurrentInputConnection()); 628 requestHideSelf(0); 629 mInputView.closing(); 630 } 631 632 private void checkToggleCapsLock() { 633 long now = System.currentTimeMillis(); 634 if (mLastShiftTime + 800 > now) { 635 mCapsLock = !mCapsLock; 636 mLastShiftTime = 0; 637 } else { 638 mLastShiftTime = now; 639 } 640 } 641 642 private String getWordSeparators() { 643 return mWordSeparators; 644 } 645 646 public boolean isWordSeparator(int code) { 647 String separators = getWordSeparators(); 648 return separators.contains(String.valueOf((char)code)); 649 } 650 651 public void pickDefaultCandidate() { 652 pickSuggestionManually(0); 653 } 654 655 public void pickSuggestionManually(int index) { 656 if (mCompletionOn && mCompletions != null && index >= 0 657 && index < mCompletions.length) { 658 CompletionInfo ci = mCompletions[index]; 659 getCurrentInputConnection().commitCompletion(ci); 660 if (mCandidateView != null) { 661 mCandidateView.clear(); 662 } 663 updateShiftKeyState(getCurrentInputEditorInfo()); 664 } else if (mComposing.length() > 0) { 665 // If we were generating candidate suggestions for the current 666 // text, we would commit one of them here. But for this sample, 667 // we will just commit the current text. 668 commitTyped(getCurrentInputConnection()); 669 } 670 } 671 672 public void swipeRight() { 673 if (mCompletionOn) { 674 pickDefaultCandidate(); 675 } 676 } 677 678 public void swipeLeft() { 679 handleBackspace(); 680 } 681 682 public void swipeDown() { 683 handleClose(); 684 } 685 686 public void swipeUp() { 687 } 688 689 public void onPress(int primaryCode) { 690 } 691 692 public void onRelease(int primaryCode) { 693 } 694 } 695