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