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