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