KeyboardState.java revision f36f90a1730f8e2838ffc72135f79f5190b83a43
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
44        public static final int UNSHIFT = 0;
45        public static final int MANUAL_SHIFT = 1;
46        public static final int AUTOMATIC_SHIFT = 2;
47        public void setShifted(int shiftMode);
48
49        public void setShiftLocked(boolean shiftLocked);
50
51        public void setSymbolsKeyboard();
52
53        public void setSymbolsShiftedKeyboard();
54
55        /**
56         * Request to call back {@link KeyboardState#onUpdateShiftState(boolean)}.
57         */
58        public void requestUpdatingShiftState();
59    }
60
61    private final SwitchActions mSwitchActions;
62
63    private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
64    private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
65
66    private static final int SWITCH_STATE_ALPHA = 0;
67    private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
68    private static final int SWITCH_STATE_SYMBOL = 2;
69    // The following states are used only on the distinct multi-touch panel devices.
70    private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
71    private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
72    private static final int SWITCH_STATE_CHORDING_ALPHA = 5;
73    private static final int SWITCH_STATE_CHORDING_SYMBOL = 6;
74    private int mSwitchState = SWITCH_STATE_ALPHA;
75    private String mLayoutSwitchBackSymbols;
76
77    private boolean mIsAlphabetMode;
78    private KeyboardShiftState mAlphabetShiftState = new KeyboardShiftState();
79    private boolean mIsSymbolShifted;
80    private boolean mPrevMainKeyboardWasShiftLocked;
81    private boolean mPrevSymbolsKeyboardWasShifted;
82
83    private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
84
85    static class SavedKeyboardState {
86        public boolean mIsValid;
87        public boolean mIsAlphabetMode;
88        public boolean mIsShiftLocked;
89        public boolean mIsShifted;
90    }
91
92    public KeyboardState(SwitchActions switchActions) {
93        mSwitchActions = switchActions;
94    }
95
96    public void onLoadKeyboard(String layoutSwitchBackSymbols) {
97        if (DEBUG_EVENT) {
98            Log.d(TAG, "onLoadKeyboard");
99        }
100        mLayoutSwitchBackSymbols = layoutSwitchBackSymbols;
101        // Reset alphabet shift state.
102        mAlphabetShiftState.setShiftLocked(false);
103        mPrevMainKeyboardWasShiftLocked = false;
104        mPrevSymbolsKeyboardWasShifted = false;
105        mShiftKeyState.onRelease();
106        mSymbolKeyState.onRelease();
107        onRestoreKeyboardState();
108    }
109
110    public void onSaveKeyboardState() {
111        final SavedKeyboardState state = mSavedKeyboardState;
112        state.mIsAlphabetMode = mIsAlphabetMode;
113        if (mIsAlphabetMode) {
114            state.mIsShiftLocked = mAlphabetShiftState.isShiftLocked();
115            state.mIsShifted = !state.mIsShiftLocked
116                    && mAlphabetShiftState.isShiftedOrShiftLocked();
117        } else {
118            state.mIsShiftLocked = false;
119            state.mIsShifted = mIsSymbolShifted;
120        }
121        state.mIsValid = true;
122        if (DEBUG_EVENT) {
123            Log.d(TAG, "onSaveKeyboardState: alphabet=" + state.mIsAlphabetMode
124                    + " shiftLocked=" + state.mIsShiftLocked + " shift=" + state.mIsShifted);
125        }
126    }
127
128    private void onRestoreKeyboardState() {
129        final SavedKeyboardState state = mSavedKeyboardState;
130        if (DEBUG_EVENT) {
131            Log.d(TAG, "onRestoreKeyboardState: valid=" + state.mIsValid
132                    + " alphabet=" + state.mIsAlphabetMode
133                    + " shiftLocked=" + state.mIsShiftLocked + " shift=" + state.mIsShifted);
134        }
135        if (!state.mIsValid || state.mIsAlphabetMode) {
136            setAlphabetKeyboard();
137        } else {
138            if (state.mIsShifted) {
139                setSymbolsShiftedKeyboard();
140            } else {
141                setSymbolsKeyboard();
142            }
143        }
144
145        if (!state.mIsValid) return;
146        state.mIsValid = false;
147
148        if (state.mIsAlphabetMode) {
149            setShiftLocked(state.mIsShiftLocked);
150            if (!state.mIsShiftLocked) {
151                setShifted(state.mIsShifted ? SwitchActions.MANUAL_SHIFT : SwitchActions.UNSHIFT);
152            }
153        }
154    }
155
156    // TODO: Remove this method.
157    public boolean isShiftLocked() {
158        return mAlphabetShiftState.isShiftLocked();
159    }
160
161    private void setShifted(int shiftMode) {
162        if (DEBUG_ACTION) {
163            Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode));
164        }
165        switch (shiftMode) {
166        case SwitchActions.AUTOMATIC_SHIFT:
167            mAlphabetShiftState.setAutomaticTemporaryUpperCase();
168            break;
169        case SwitchActions.MANUAL_SHIFT:
170            mAlphabetShiftState.setShifted(true);
171            break;
172        case SwitchActions.UNSHIFT:
173            mAlphabetShiftState.setShifted(false);
174            break;
175        }
176        mSwitchActions.setShifted(shiftMode);
177    }
178
179    private void setShiftLocked(boolean shiftLocked) {
180        if (DEBUG_ACTION) {
181            Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked);
182        }
183        mAlphabetShiftState.setShiftLocked(shiftLocked);
184        mSwitchActions.setShiftLocked(shiftLocked);
185    }
186
187    private void toggleAlphabetAndSymbols() {
188        if (mIsAlphabetMode) {
189            setSymbolsKeyboard();
190        } else {
191            setAlphabetKeyboard();
192        }
193    }
194
195    private void toggleShiftInSymbols() {
196        if (mIsSymbolShifted) {
197            setSymbolsKeyboard();
198        } else {
199            setSymbolsShiftedKeyboard();
200        }
201    }
202
203    private void setAlphabetKeyboard() {
204        if (DEBUG_ACTION) {
205            Log.d(TAG, "setAlphabetKeyboard");
206        }
207        mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
208        mSwitchActions.setAlphabetKeyboard();
209        mIsAlphabetMode = true;
210        mIsSymbolShifted = false;
211        mSwitchState = SWITCH_STATE_ALPHA;
212        setShiftLocked(mPrevMainKeyboardWasShiftLocked);
213        mPrevMainKeyboardWasShiftLocked = false;
214        mSwitchActions.requestUpdatingShiftState();
215    }
216
217    // TODO: Make this method private
218    public void setSymbolsKeyboard() {
219        mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
220        if (mPrevSymbolsKeyboardWasShifted) {
221            setSymbolsShiftedKeyboard();
222            return;
223        }
224
225        if (DEBUG_ACTION) {
226            Log.d(TAG, "setSymbolsKeyboard");
227        }
228        mSwitchActions.setSymbolsKeyboard();
229        mIsAlphabetMode = false;
230        mIsSymbolShifted = false;
231        // Reset alphabet shift state.
232        mAlphabetShiftState.setShiftLocked(false);
233        mPrevSymbolsKeyboardWasShifted = false;
234        mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
235    }
236
237    private void setSymbolsShiftedKeyboard() {
238        if (DEBUG_ACTION) {
239            Log.d(TAG, "setSymbolsShiftedKeyboard");
240        }
241        mSwitchActions.setSymbolsShiftedKeyboard();
242        mIsAlphabetMode = false;
243        mIsSymbolShifted = true;
244        // Reset alphabet shift state.
245        mAlphabetShiftState.setShiftLocked(false);
246        mPrevSymbolsKeyboardWasShifted = false;
247        mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
248    }
249
250    public void onPressKey(int code) {
251        if (DEBUG_EVENT) {
252            Log.d(TAG, "onPressKey: code=" + Keyboard.printableCode(code) + " " + this);
253        }
254        if (code == Keyboard.CODE_SHIFT) {
255            onPressShift();
256        } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
257            onPressSymbol();
258        } else {
259            mShiftKeyState.onOtherKeyPressed();
260            mSymbolKeyState.onOtherKeyPressed();
261        }
262    }
263
264    public void onReleaseKey(int code, boolean withSliding) {
265        if (DEBUG_EVENT) {
266            Log.d(TAG, "onReleaseKey: code=" + Keyboard.printableCode(code)
267                    + " sliding=" + withSliding + " " + this);
268        }
269        if (code == Keyboard.CODE_SHIFT) {
270            onReleaseShift(withSliding);
271        } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
272            // TODO: Make use of withSliding instead of relying on mSwitchState.
273            onReleaseSymbol();
274        }
275    }
276
277    private void onPressSymbol() {
278        toggleAlphabetAndSymbols();
279        mSymbolKeyState.onPress();
280        mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
281    }
282
283    private void onReleaseSymbol() {
284        // Switch back to the previous keyboard mode if the user chords the mode change key and
285        // another key, then releases the mode change key.
286        if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
287            toggleAlphabetAndSymbols();
288        }
289        mSymbolKeyState.onRelease();
290    }
291
292    public void onUpdateShiftState(boolean autoCaps) {
293        if (DEBUG_EVENT) {
294            Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + " " + this);
295        }
296        onUpdateShiftStateInternal(autoCaps);
297    }
298
299    private void onUpdateShiftStateInternal(boolean autoCaps) {
300        if (mIsAlphabetMode) {
301            if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
302                if (mShiftKeyState.isReleasing() && autoCaps) {
303                    // Only when shift key is releasing, automatic temporary upper case will be set.
304                    setShifted(SwitchActions.AUTOMATIC_SHIFT);
305                } else {
306                    setShifted(mShiftKeyState.isMomentary()
307                            ? SwitchActions.MANUAL_SHIFT : SwitchActions.UNSHIFT);
308                }
309            }
310        } else {
311            // In symbol keyboard mode, we should clear shift key state because only alphabet
312            // keyboard has shift key.
313            mSymbolKeyState.onRelease();
314        }
315    }
316
317    private void onPressShift() {
318        if (mIsAlphabetMode) {
319            if (mAlphabetShiftState.isShiftLocked()) {
320                // Shift key is pressed while caps lock state, we will treat this state as shifted
321                // caps lock state and mark as if shift key pressed while normal state.
322                setShifted(SwitchActions.MANUAL_SHIFT);
323                mShiftKeyState.onPress();
324            } else if (mAlphabetShiftState.isAutomaticTemporaryUpperCase()) {
325                // Shift key is pressed while automatic temporary upper case, we have to move to
326                // manual temporary upper case.
327                setShifted(SwitchActions.MANUAL_SHIFT);
328                mShiftKeyState.onPress();
329            } else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
330                // In manual upper case state, we just record shift key has been pressing while
331                // shifted state.
332                mShiftKeyState.onPressOnShifted();
333            } else {
334                // In base layout, chording or manual temporary upper case mode is started.
335                setShifted(SwitchActions.MANUAL_SHIFT);
336                mShiftKeyState.onPress();
337            }
338        } else {
339            // In symbol mode, just toggle symbol and symbol more keyboard.
340            toggleShiftInSymbols();
341            mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
342            mShiftKeyState.onPress();
343        }
344    }
345
346    private void onReleaseShift(boolean withSliding) {
347        if (mIsAlphabetMode) {
348            final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
349            if (mShiftKeyState.isMomentary()) {
350                // After chording input while normal state.
351                if (mAlphabetShiftState.isShiftLockShifted()) {
352                    setShiftLocked(true);
353                } else {
354                    setShifted(SwitchActions.UNSHIFT);
355                }
356            } else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
357                    && (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
358                    && !withSliding) {
359                // Shift has been long pressed, ignore this release.
360            } else if (isShiftLocked && !mShiftKeyState.isIgnoring() && !withSliding) {
361                // Shift has been pressed without chording while caps lock state.
362                setShiftLocked(false);
363            } else if (mAlphabetShiftState.isShiftedOrShiftLocked()
364                    && mShiftKeyState.isPressingOnShifted() && !withSliding) {
365                // Shift has been pressed without chording while shifted state.
366                setShifted(SwitchActions.UNSHIFT);
367            } else if (mAlphabetShiftState.isManualTemporaryUpperCaseFromAuto()
368                    && mShiftKeyState.isPressing() && !withSliding) {
369                // Shift has been pressed without chording while manual temporary upper case
370                // transited from automatic temporary upper case.
371                setShifted(SwitchActions.UNSHIFT);
372            }
373        } else {
374            // In symbol mode, switch back to the previous keyboard mode if the user chords the
375            // shift key and another key, then releases the shift key.
376            if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
377                toggleShiftInSymbols();
378            }
379        }
380        mShiftKeyState.onRelease();
381    }
382
383    public void onCancelInput(boolean isSinglePointer) {
384        if (DEBUG_EVENT) {
385            Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
386        }
387        // Switch back to the previous keyboard mode if the user cancels sliding input.
388        if (isSinglePointer) {
389            if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
390                toggleAlphabetAndSymbols();
391            } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
392                toggleShiftInSymbols();
393            }
394        }
395    }
396
397    public boolean isInMomentarySwitchState() {
398        return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
399                || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
400    }
401
402    private static boolean isSpaceCharacter(int c) {
403        return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
404    }
405
406    private boolean isLayoutSwitchBackCharacter(int c) {
407        if (TextUtils.isEmpty(mLayoutSwitchBackSymbols)) return false;
408        if (mLayoutSwitchBackSymbols.indexOf(c) >= 0) return true;
409        return false;
410    }
411
412    public void onCodeInput(int code, boolean isSinglePointer, boolean autoCaps) {
413        if (DEBUG_EVENT) {
414            Log.d(TAG, "onCodeInput: code=" + Keyboard.printableCode(code)
415                    + " single=" + isSinglePointer
416                    + " autoCaps=" + autoCaps + " " + this);
417        }
418
419        if (mIsAlphabetMode && code == Keyboard.CODE_CAPSLOCK) {
420            if (mAlphabetShiftState.isShiftLocked()) {
421                setShiftLocked(false);
422                // Shift key is long pressed or double tapped while caps lock state, we will
423                // toggle back to normal state. And mark as if shift key is released.
424                mShiftKeyState.onRelease();
425            } else {
426                setShiftLocked(true);
427            }
428        }
429
430        switch (mSwitchState) {
431        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
432            // Only distinct multi touch devices can be in this state.
433            // On non-distinct multi touch devices, mode change key is handled by
434            // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and
435            // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts
436            // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from
437            // {@link #SWITCH_STATE_MOMENTARY}.
438            if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
439                // Detected only the mode change key has been pressed, and then released.
440                if (mIsAlphabetMode) {
441                    mSwitchState = SWITCH_STATE_ALPHA;
442                } else {
443                    mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
444                }
445            } else if (isSinglePointer) {
446                // Switch back to the previous keyboard mode if the user pressed the mode change key
447                // and slid to other key, then released the finger.
448                // If the user cancels the sliding input, switching back to the previous keyboard
449                // mode is handled by {@link #onCancelInput}.
450                toggleAlphabetAndSymbols();
451            } else {
452                // Chording input is being started. The keyboard mode will be switched back to the
453                // previous mode in {@link onReleaseSymbol} when the mode change key is released.
454                mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
455            }
456            break;
457        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
458            if (code == Keyboard.CODE_SHIFT) {
459                // Detected only the shift key has been pressed on symbol layout, and then released.
460                mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
461            } else if (isSinglePointer) {
462                // Switch back to the previous keyboard mode if the user pressed the shift key on
463                // symbol mode and slid to other key, then released the finger.
464                toggleShiftInSymbols();
465                mSwitchState = SWITCH_STATE_SYMBOL;
466            } else {
467                // Chording input is being started. The keyboard mode will be switched back to the
468                // previous mode in {@link onReleaseShift} when the shift key is released.
469                mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
470            }
471            break;
472        case SWITCH_STATE_SYMBOL_BEGIN:
473            if (!isSpaceCharacter(code) && (Keyboard.isLetterCode(code)
474                    || code == Keyboard.CODE_OUTPUT_TEXT)) {
475                mSwitchState = SWITCH_STATE_SYMBOL;
476            }
477            // Switch back to alpha keyboard mode immediately if user types a quote character.
478            if (isLayoutSwitchBackCharacter(code)) {
479                setAlphabetKeyboard();
480            }
481            break;
482        case SWITCH_STATE_SYMBOL:
483        case SWITCH_STATE_CHORDING_SYMBOL:
484            // Switch back to alpha keyboard mode if user types one or more non-space/enter
485            // characters followed by a space/enter or a quote character.
486            if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) {
487                setAlphabetKeyboard();
488            }
489            break;
490        }
491
492        // If the code is a letter, update keyboard shift state.
493        if (Keyboard.isLetterCode(code)) {
494            onUpdateShiftStateInternal(autoCaps);
495        }
496    }
497
498    private static String shiftModeToString(int shiftMode) {
499        switch (shiftMode) {
500        case SwitchActions.UNSHIFT: return "UNSHIFT";
501        case SwitchActions.MANUAL_SHIFT: return "MANUAL";
502        case SwitchActions.AUTOMATIC_SHIFT: return "AUTOMATIC";
503        default: return null;
504        }
505    }
506
507    private static String switchStateToString(int switchState) {
508        switch (switchState) {
509        case SWITCH_STATE_ALPHA: return "ALPHA";
510        case SWITCH_STATE_SYMBOL_BEGIN: return "SYMBOL-BEGIN";
511        case SWITCH_STATE_SYMBOL: return "SYMBOL";
512        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
513        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
514        case SWITCH_STATE_CHORDING_ALPHA: return "CHORDING-ALPHA";
515        case SWITCH_STATE_CHORDING_SYMBOL: return "CHORDING-SYMBOL";
516        default: return null;
517        }
518    }
519
520    @Override
521    public String toString() {
522        return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
523                        : (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
524                + " shift=" + mShiftKeyState
525                + " symbol=" + mSymbolKeyState
526                + " switch=" + switchStateToString(mSwitchState) + "]";
527    }
528}
529