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