KeyboardState.java revision f057b25cddb3e5e18bc56e8ddaab541044c6ba58
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.keyboard.internal;
18
19import android.text.TextUtils;
20import android.util.Log;
21
22import com.android.inputmethod.keyboard.Keyboard;
23
24/**
25 * Keyboard state machine.
26 *
27 * This class contains all keyboard state transition logic.
28 *
29 * The input events are {@link #onLoadKeyboard(String)}, {@link #onSaveKeyboardState()},
30 * {@link #onPressKey(int)}, {@link #onReleaseKey(int, boolean)},
31 * {@link #onCodeInput(int, boolean, boolean)}, {@link #onCancelInput(boolean)},
32 * {@link #onUpdateShiftState(boolean)}.
33 *
34 * The actions are {@link SwitchActions}'s methods.
35 */
36public class KeyboardState {
37    private static final String TAG = KeyboardState.class.getSimpleName();
38    private static final boolean DEBUG_EVENT = false;
39    private static final boolean DEBUG_ACTION = false;
40
41    public interface SwitchActions {
42        public void setAlphabetKeyboard();
43        public void setAlphabetManualShiftedKeyboard();
44        public void setAlphabetAutomaticShiftedKeyboard();
45        public void setAlphabetShiftLockedKeyboard();
46        public void setAlphabetShiftLockShiftedKeyboard();
47        public void setSymbolsKeyboard();
48        public void setSymbolsShiftedKeyboard();
49
50        /**
51         * Request to call back {@link KeyboardState#onUpdateShiftState(boolean)}.
52         */
53        public void requestUpdatingShiftState();
54    }
55
56    private final SwitchActions mSwitchActions;
57
58    private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
59    private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
60
61    // TODO: Merge {@link #mSwitchState}, {@link #mIsAlphabetMode}, {@link #mAlphabetShiftState},
62    // {@link #mIsSymbolShifted}, {@link #mPrevMainKeyboardWasShiftLocked}, and
63    // {@link #mPrevSymbolsKeyboardWasShifted} into single state variable.
64    private static final int SWITCH_STATE_ALPHA = 0;
65    private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
66    private static final int SWITCH_STATE_SYMBOL = 2;
67    private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
68    private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
69    private int mSwitchState = SWITCH_STATE_ALPHA;
70    private String mLayoutSwitchBackSymbols;
71
72    private boolean mIsAlphabetMode;
73    private KeyboardShiftState mAlphabetShiftState = new KeyboardShiftState();
74    private boolean mIsSymbolShifted;
75    private boolean mPrevMainKeyboardWasShiftLocked;
76    private boolean mPrevSymbolsKeyboardWasShifted;
77
78    private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
79
80    static class SavedKeyboardState {
81        public boolean mIsValid;
82        public boolean mIsAlphabetMode;
83        public boolean mIsShiftLocked;
84        public boolean mIsShifted;
85
86        @Override
87        public String toString() {
88            if (!mIsValid) return "INVALID";
89            if (mIsAlphabetMode) {
90                if (mIsShiftLocked) return "ALPHABET_SHIFT_LOCKED";
91                return mIsShifted ? "ALPHABET_SHIFTED" : "ALPHABET";
92            } else {
93                return mIsShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS";
94            }
95        }
96    }
97
98    public KeyboardState(SwitchActions switchActions) {
99        mSwitchActions = switchActions;
100    }
101
102    public void onLoadKeyboard(String layoutSwitchBackSymbols) {
103        if (DEBUG_EVENT) {
104            Log.d(TAG, "onLoadKeyboard: " + this);
105        }
106        mLayoutSwitchBackSymbols = layoutSwitchBackSymbols;
107        // Reset alphabet shift state.
108        mAlphabetShiftState.setShiftLocked(false);
109        mPrevMainKeyboardWasShiftLocked = false;
110        mPrevSymbolsKeyboardWasShifted = false;
111        mShiftKeyState.onRelease();
112        mSymbolKeyState.onRelease();
113        onRestoreKeyboardState();
114    }
115
116    public void onSaveKeyboardState() {
117        final SavedKeyboardState state = mSavedKeyboardState;
118        state.mIsAlphabetMode = mIsAlphabetMode;
119        if (mIsAlphabetMode) {
120            state.mIsShiftLocked = mAlphabetShiftState.isShiftLocked();
121            state.mIsShifted = !state.mIsShiftLocked
122                    && mAlphabetShiftState.isShiftedOrShiftLocked();
123        } else {
124            state.mIsShiftLocked = false;
125            state.mIsShifted = mIsSymbolShifted;
126        }
127        state.mIsValid = true;
128        if (DEBUG_EVENT) {
129            Log.d(TAG, "onSaveKeyboardState: saved=" + state + " " + this);
130        }
131    }
132
133    private void onRestoreKeyboardState() {
134        final SavedKeyboardState state = mSavedKeyboardState;
135        if (DEBUG_EVENT) {
136            Log.d(TAG, "onRestoreKeyboardState: saved=" + state + " " + this);
137        }
138        if (!state.mIsValid || state.mIsAlphabetMode) {
139            setAlphabetKeyboard();
140        } else {
141            if (state.mIsShifted) {
142                setSymbolsShiftedKeyboard();
143            } else {
144                setSymbolsKeyboard();
145            }
146        }
147
148        if (!state.mIsValid) return;
149        state.mIsValid = false;
150
151        if (state.mIsAlphabetMode) {
152            setShiftLocked(state.mIsShiftLocked);
153            if (!state.mIsShiftLocked) {
154                setShifted(state.mIsShifted ? MANUAL_SHIFT : UNSHIFT);
155            }
156        }
157    }
158
159    private static final int UNSHIFT = 0;
160    private static final int MANUAL_SHIFT = 1;
161    private static final int AUTOMATIC_SHIFT = 2;
162    private static final int SHIFT_LOCK_SHIFTED = 3;
163
164    private void setShifted(int shiftMode) {
165        if (DEBUG_ACTION) {
166            Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode) + " " + this);
167        }
168        if (!mIsAlphabetMode) return;
169        final int prevShiftMode;
170        if (mAlphabetShiftState.isAutomaticTemporaryUpperCase()) {
171            prevShiftMode = AUTOMATIC_SHIFT;
172        } else if (mAlphabetShiftState.isManualTemporaryUpperCase()) {
173            prevShiftMode = MANUAL_SHIFT;
174        } else {
175            prevShiftMode = UNSHIFT;
176        }
177        switch (shiftMode) {
178        case AUTOMATIC_SHIFT:
179            mAlphabetShiftState.setAutomaticTemporaryUpperCase();
180            if (shiftMode != prevShiftMode) {
181                mSwitchActions.setAlphabetAutomaticShiftedKeyboard();
182            }
183            break;
184        case MANUAL_SHIFT:
185            mAlphabetShiftState.setShifted(true);
186            if (shiftMode != prevShiftMode) {
187                mSwitchActions.setAlphabetManualShiftedKeyboard();
188            }
189            break;
190        case UNSHIFT:
191            mAlphabetShiftState.setShifted(false);
192            if (shiftMode != prevShiftMode) {
193                mSwitchActions.setAlphabetKeyboard();
194            }
195            break;
196        case SHIFT_LOCK_SHIFTED:
197            mAlphabetShiftState.setShifted(true);
198            mSwitchActions.setAlphabetShiftLockShiftedKeyboard();
199            break;
200        }
201    }
202
203    private void setShiftLocked(boolean shiftLocked) {
204        if (DEBUG_ACTION) {
205            Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked + " " + this);
206        }
207        if (!mIsAlphabetMode) return;
208        if (shiftLocked && (!mAlphabetShiftState.isShiftLocked()
209                || mAlphabetShiftState.isShiftLockShifted())) {
210            mSwitchActions.setAlphabetShiftLockedKeyboard();
211        }
212        if (!shiftLocked && mAlphabetShiftState.isShiftLocked()) {
213            mSwitchActions.setAlphabetKeyboard();
214        }
215        mAlphabetShiftState.setShiftLocked(shiftLocked);
216    }
217
218    private void toggleAlphabetAndSymbols() {
219        if (DEBUG_ACTION) {
220            Log.d(TAG, "toggleAlphabetAndSymbols: " + this);
221        }
222        if (mIsAlphabetMode) {
223            mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
224            if (mPrevSymbolsKeyboardWasShifted) {
225                setSymbolsShiftedKeyboard();
226            } else {
227                setSymbolsKeyboard();
228            }
229            mPrevSymbolsKeyboardWasShifted = false;
230        } else {
231            mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
232            setAlphabetKeyboard();
233            if (mPrevMainKeyboardWasShiftLocked) {
234                setShiftLocked(true);
235            }
236            mPrevMainKeyboardWasShiftLocked = false;
237        }
238    }
239
240    private void toggleShiftInSymbols() {
241        if (mIsSymbolShifted) {
242            setSymbolsKeyboard();
243        } else {
244            setSymbolsShiftedKeyboard();
245        }
246    }
247
248    private void setAlphabetKeyboard() {
249        if (DEBUG_ACTION) {
250            Log.d(TAG, "setAlphabetKeyboard");
251        }
252        mSwitchActions.setAlphabetKeyboard();
253        mIsAlphabetMode = true;
254        mIsSymbolShifted = false;
255        mSwitchState = SWITCH_STATE_ALPHA;
256        mSwitchActions.requestUpdatingShiftState();
257    }
258
259    // TODO: Make this method private
260    public void setSymbolsKeyboard() {
261        if (DEBUG_ACTION) {
262            Log.d(TAG, "setSymbolsKeyboard");
263        }
264        mSwitchActions.setSymbolsKeyboard();
265        mIsAlphabetMode = false;
266        mIsSymbolShifted = false;
267        // Reset alphabet shift state.
268        mAlphabetShiftState.setShiftLocked(false);
269        mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
270    }
271
272    private void setSymbolsShiftedKeyboard() {
273        if (DEBUG_ACTION) {
274            Log.d(TAG, "setSymbolsShiftedKeyboard");
275        }
276        mSwitchActions.setSymbolsShiftedKeyboard();
277        mIsAlphabetMode = false;
278        mIsSymbolShifted = true;
279        // Reset alphabet shift state.
280        mAlphabetShiftState.setShiftLocked(false);
281        mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
282    }
283
284    public void onPressKey(int code) {
285        if (DEBUG_EVENT) {
286            Log.d(TAG, "onPressKey: code=" + Keyboard.printableCode(code) + " " + this);
287        }
288        if (code == Keyboard.CODE_SHIFT) {
289            onPressShift();
290        } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
291            onPressSymbol();
292        } else {
293            mShiftKeyState.onOtherKeyPressed();
294            mSymbolKeyState.onOtherKeyPressed();
295        }
296    }
297
298    public void onReleaseKey(int code, boolean withSliding) {
299        if (DEBUG_EVENT) {
300            Log.d(TAG, "onReleaseKey: code=" + Keyboard.printableCode(code)
301                    + " sliding=" + withSliding + " " + this);
302        }
303        if (code == Keyboard.CODE_SHIFT) {
304            onReleaseShift(withSliding);
305        } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
306            onReleaseSymbol(withSliding);
307        }
308    }
309
310    private void onPressSymbol() {
311        toggleAlphabetAndSymbols();
312        mSymbolKeyState.onPress();
313        mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
314    }
315
316    private void onReleaseSymbol(boolean withSliding) {
317        if (mSymbolKeyState.isChording()) {
318            // Switch back to the previous keyboard mode if the user chords the mode change key and
319            // another key, then releases the mode change key.
320            toggleAlphabetAndSymbols();
321        } else if (!withSliding) {
322            // If the mode change key is being released without sliding, we should forget the
323            // previous symbols keyboard shift state and simply switch back to symbols layout
324            // (never symbols shifted) next time the mode gets changed to symbols layout.
325            mPrevSymbolsKeyboardWasShifted = false;
326        }
327        mSymbolKeyState.onRelease();
328    }
329
330    public void onUpdateShiftState(boolean autoCaps) {
331        if (DEBUG_EVENT) {
332            Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + " " + this);
333        }
334        updateAlphabetShiftState(autoCaps);
335    }
336
337    private void updateAlphabetShiftState(boolean autoCaps) {
338        if (!mIsAlphabetMode) return;
339        if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
340            if (mShiftKeyState.isReleasing() && autoCaps) {
341                // Only when shift key is releasing, automatic temporary upper case will be set.
342                setShifted(AUTOMATIC_SHIFT);
343            } else {
344                setShifted(mShiftKeyState.isChording() ? MANUAL_SHIFT : UNSHIFT);
345            }
346        }
347    }
348
349    private void onPressShift() {
350        if (mIsAlphabetMode) {
351            if (mAlphabetShiftState.isShiftLocked()) {
352                // Shift key is pressed while caps lock state, we will treat this state as shifted
353                // caps lock state and mark as if shift key pressed while normal state.
354                setShifted(SHIFT_LOCK_SHIFTED);
355                mShiftKeyState.onPress();
356            } else if (mAlphabetShiftState.isAutomaticTemporaryUpperCase()) {
357                // Shift key is pressed while automatic temporary upper case, we have to move to
358                // manual temporary upper case.
359                setShifted(MANUAL_SHIFT);
360                mShiftKeyState.onPress();
361            } else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
362                // In manual upper case state, we just record shift key has been pressing while
363                // shifted state.
364                mShiftKeyState.onPressOnShifted();
365            } else {
366                // In base layout, chording or manual temporary upper case mode is started.
367                setShifted(MANUAL_SHIFT);
368                mShiftKeyState.onPress();
369            }
370        } else {
371            // In symbol mode, just toggle symbol and symbol more keyboard.
372            toggleShiftInSymbols();
373            mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
374            mShiftKeyState.onPress();
375        }
376    }
377
378    private void onReleaseShift(boolean withSliding) {
379        if (mIsAlphabetMode) {
380            final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
381            if (mShiftKeyState.isChording()) {
382                if (mAlphabetShiftState.isShiftLockShifted()) {
383                    // After chording input while caps lock state.
384                    setShiftLocked(true);
385                } else {
386                    // After chording input while normal state.
387                    setShifted(UNSHIFT);
388                }
389            } else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) {
390                // In caps lock state, shift has been pressed and slid out to other key.
391                setShiftLocked(true);
392            } else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
393                    && (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
394                    && !withSliding) {
395                // Shift has been long pressed, ignore this release.
396            } else if (isShiftLocked && !mShiftKeyState.isIgnoring() && !withSliding) {
397                // Shift has been pressed without chording while caps lock state.
398                setShiftLocked(false);
399            } else if (mAlphabetShiftState.isShiftedOrShiftLocked()
400                    && mShiftKeyState.isPressingOnShifted() && !withSliding) {
401                // Shift has been pressed without chording while shifted state.
402                setShifted(UNSHIFT);
403            } else if (mAlphabetShiftState.isManualTemporaryUpperCaseFromAuto()
404                    && mShiftKeyState.isPressing() && !withSliding) {
405                // Shift has been pressed without chording while manual temporary upper case
406                // transited from automatic temporary upper case.
407                setShifted(UNSHIFT);
408            }
409        } else {
410            // In symbol mode, switch back to the previous keyboard mode if the user chords the
411            // shift key and another key, then releases the shift key.
412            if (mShiftKeyState.isChording()) {
413                toggleShiftInSymbols();
414            }
415        }
416        mShiftKeyState.onRelease();
417    }
418
419    public void onCancelInput(boolean isSinglePointer) {
420        if (DEBUG_EVENT) {
421            Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
422        }
423        // Switch back to the previous keyboard mode if the user cancels sliding input.
424        if (isSinglePointer) {
425            if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
426                toggleAlphabetAndSymbols();
427            } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
428                toggleShiftInSymbols();
429            }
430        }
431    }
432
433    public boolean isInMomentarySwitchState() {
434        return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
435                || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
436    }
437
438    private static boolean isSpaceCharacter(int c) {
439        return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
440    }
441
442    private boolean isLayoutSwitchBackCharacter(int c) {
443        if (TextUtils.isEmpty(mLayoutSwitchBackSymbols)) return false;
444        if (mLayoutSwitchBackSymbols.indexOf(c) >= 0) return true;
445        return false;
446    }
447
448    public void onCodeInput(int code, boolean isSinglePointer, boolean autoCaps) {
449        if (DEBUG_EVENT) {
450            Log.d(TAG, "onCodeInput: code=" + Keyboard.printableCode(code)
451                    + " single=" + isSinglePointer
452                    + " autoCaps=" + autoCaps + " " + this);
453        }
454
455        if (mIsAlphabetMode && code == Keyboard.CODE_CAPSLOCK) {
456            if (mAlphabetShiftState.isShiftLocked()) {
457                setShiftLocked(false);
458                // Shift key is long pressed or double tapped while caps lock state, we will
459                // toggle back to normal state. And mark as if shift key is released.
460                mShiftKeyState.onRelease();
461            } else {
462                setShiftLocked(true);
463            }
464        }
465
466        switch (mSwitchState) {
467        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
468            if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
469                // Detected only the mode change key has been pressed, and then released.
470                if (mIsAlphabetMode) {
471                    mSwitchState = SWITCH_STATE_ALPHA;
472                } else {
473                    mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
474                }
475            } else if (isSinglePointer) {
476                // Switch back to the previous keyboard mode if the user pressed the mode change key
477                // and slid to other key, then released the finger.
478                // If the user cancels the sliding input, switching back to the previous keyboard
479                // mode is handled by {@link #onCancelInput}.
480                toggleAlphabetAndSymbols();
481            }
482            break;
483        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
484            if (code == Keyboard.CODE_SHIFT) {
485                // Detected only the shift key has been pressed on symbol layout, and then released.
486                mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
487            } else if (isSinglePointer) {
488                // Switch back to the previous keyboard mode if the user pressed the shift key on
489                // symbol mode and slid to other key, then released the finger.
490                toggleShiftInSymbols();
491                mSwitchState = SWITCH_STATE_SYMBOL;
492            }
493            break;
494        case SWITCH_STATE_SYMBOL_BEGIN:
495            if (!isSpaceCharacter(code) && (Keyboard.isLetterCode(code)
496                    || code == Keyboard.CODE_OUTPUT_TEXT)) {
497                mSwitchState = SWITCH_STATE_SYMBOL;
498            }
499            // Switch back to alpha keyboard mode immediately if user types a quote character.
500            if (isLayoutSwitchBackCharacter(code)) {
501                toggleAlphabetAndSymbols();
502            }
503            break;
504        case SWITCH_STATE_SYMBOL:
505            // Switch back to alpha keyboard mode if user types one or more non-space/enter
506            // characters followed by a space/enter or a quote character.
507            if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) {
508                toggleAlphabetAndSymbols();
509            }
510            break;
511        }
512
513        // If the code is a letter, update keyboard shift state.
514        if (Keyboard.isLetterCode(code)) {
515            updateAlphabetShiftState(autoCaps);
516        }
517    }
518
519    private static String shiftModeToString(int shiftMode) {
520        switch (shiftMode) {
521        case UNSHIFT: return "UNSHIFT";
522        case MANUAL_SHIFT: return "MANUAL";
523        case AUTOMATIC_SHIFT: return "AUTOMATIC";
524        default: return null;
525        }
526    }
527
528    private static String switchStateToString(int switchState) {
529        switch (switchState) {
530        case SWITCH_STATE_ALPHA: return "ALPHA";
531        case SWITCH_STATE_SYMBOL_BEGIN: return "SYMBOL-BEGIN";
532        case SWITCH_STATE_SYMBOL: return "SYMBOL";
533        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
534        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
535        default: return null;
536        }
537    }
538
539    @Override
540    public String toString() {
541        return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
542                        : (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
543                + " shift=" + mShiftKeyState
544                + " symbol=" + mSymbolKeyState
545                + " switch=" + switchStateToString(mSwitchState) + "]";
546    }
547}
548