MainKeyboardAccessibilityDelegate.java revision 1e3167229519843b83ba8bea7d78a82ffba236bc
1/*
2 * Copyright (C) 2014 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
17package com.android.inputmethod.accessibility;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.os.SystemClock;
22import android.util.Log;
23import android.util.SparseIntArray;
24import android.view.MotionEvent;
25
26import com.android.inputmethod.keyboard.Key;
27import com.android.inputmethod.keyboard.KeyDetector;
28import com.android.inputmethod.keyboard.Keyboard;
29import com.android.inputmethod.keyboard.KeyboardId;
30import com.android.inputmethod.keyboard.MainKeyboardView;
31import com.android.inputmethod.keyboard.PointerTracker;
32import com.android.inputmethod.latin.R;
33import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
34
35/**
36 * This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
37 * accessibility support via composition rather via inheritance.
38 */
39public final class MainKeyboardAccessibilityDelegate
40        extends KeyboardAccessibilityDelegate<MainKeyboardView>
41        implements AccessibilityLongPressTimer.LongPressTimerCallback {
42    private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName();
43
44    /** Map of keyboard modes to resource IDs. */
45    private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
46
47    static {
48        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date);
49        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time);
50        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email);
51        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im);
52        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number);
53        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone);
54        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text);
55        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time);
56        KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url);
57    }
58
59    /** The most recently set keyboard mode. */
60    private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
61    private static final int KEYBOARD_IS_HIDDEN = -1;
62    // The rectangle region to ignore hover events.
63    private final Rect mBoundsToIgnoreHoverEvent = new Rect();
64
65    private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
66
67    public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView,
68            final KeyDetector keyDetector) {
69        super(mainKeyboardView, keyDetector);
70        mAccessibilityLongPressTimer = new AccessibilityLongPressTimer(
71                this /* callback */, mainKeyboardView.getContext());
72    }
73
74    /**
75     * {@inheritDoc}
76     */
77    @Override
78    public void setKeyboard(final Keyboard keyboard) {
79        if (keyboard == null) {
80            return;
81        }
82        final Keyboard lastKeyboard = getKeyboard();
83        super.setKeyboard(keyboard);
84        final int lastKeyboardMode = mLastKeyboardMode;
85        mLastKeyboardMode = keyboard.mId.mMode;
86
87        // Since this method is called even when accessibility is off, make sure
88        // to check the state before announcing anything.
89        if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
90            return;
91        }
92        // Announce the language name only when the language is changed.
93        if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) {
94            announceKeyboardLanguage(keyboard);
95            return;
96        }
97        // Announce the mode only when the mode is changed.
98        if (keyboard.mId.mMode != lastKeyboardMode) {
99            announceKeyboardMode(keyboard);
100            return;
101        }
102        // Announce the keyboard type only when the type is changed.
103        if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
104            announceKeyboardType(keyboard, lastKeyboard);
105            return;
106        }
107    }
108
109    /**
110     * Called when the keyboard is hidden and accessibility is enabled.
111     */
112    public void onHideWindow() {
113        announceKeyboardHidden();
114        mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
115    }
116
117    /**
118     * Announces which language of keyboard is being displayed.
119     *
120     * @param keyboard The new keyboard.
121     */
122    private void announceKeyboardLanguage(final Keyboard keyboard) {
123        final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
124                keyboard.mId.mSubtype);
125        sendWindowStateChanged(languageText);
126    }
127
128    /**
129     * Announces which type of keyboard is being displayed.
130     * If the keyboard type is unknown, no announcement is made.
131     *
132     * @param keyboard The new keyboard.
133     */
134    private void announceKeyboardMode(final Keyboard keyboard) {
135        final Context context = mKeyboardView.getContext();
136        final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode);
137        if (modeTextResId == 0) {
138            return;
139        }
140        final String modeText = context.getString(modeTextResId);
141        final String text = context.getString(R.string.announce_keyboard_mode, modeText);
142        sendWindowStateChanged(text);
143    }
144
145    /**
146     * Announces which type of keyboard is being displayed.
147     *
148     * @param keyboard The new keyboard.
149     * @param lastKeyboard The last keyboard.
150     */
151    private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) {
152        final int lastElementId = lastKeyboard.mId.mElementId;
153        final int resId;
154        switch (keyboard.mId.mElementId) {
155        case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
156        case KeyboardId.ELEMENT_ALPHABET:
157            if (lastElementId == KeyboardId.ELEMENT_ALPHABET
158                    || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
159                // Transition between alphabet mode and automatic shifted mode should be silently
160                // ignored because it can be determined by each key's talk back announce.
161                return;
162            }
163            resId = R.string.spoken_description_mode_alpha;
164            break;
165        case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
166            if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
167                // Resetting automatic shifted mode by pressing the shift key causes the transition
168                // from automatic shifted to manual shifted that should be silently ignored.
169                return;
170            }
171            resId = R.string.spoken_description_shiftmode_on;
172            break;
173        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
174            if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
175                // Resetting caps locked mode by pressing the shift key causes the transition
176                // from shift locked to shift lock shifted that should be silently ignored.
177                return;
178            }
179            resId = R.string.spoken_description_shiftmode_locked;
180            break;
181        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
182            resId = R.string.spoken_description_shiftmode_locked;
183            break;
184        case KeyboardId.ELEMENT_SYMBOLS:
185            resId = R.string.spoken_description_mode_symbol;
186            break;
187        case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
188            resId = R.string.spoken_description_mode_symbol_shift;
189            break;
190        case KeyboardId.ELEMENT_PHONE:
191            resId = R.string.spoken_description_mode_phone;
192            break;
193        case KeyboardId.ELEMENT_PHONE_SYMBOLS:
194            resId = R.string.spoken_description_mode_phone_shift;
195            break;
196        default:
197            return;
198        }
199        sendWindowStateChanged(resId);
200    }
201
202    /**
203     * Announces that the keyboard has been hidden.
204     */
205    private void announceKeyboardHidden() {
206        sendWindowStateChanged(R.string.announce_keyboard_hidden);
207    }
208
209    @Override
210    public void performClickOn(final Key key) {
211        final int x = key.getHitBox().centerX();
212        final int y = key.getHitBox().centerY();
213        if (DEBUG_HOVER) {
214            Log.d(TAG, "performClickOn: key=" + key
215                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
216        }
217        if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
218            // This hover exit event points to the key that should be ignored.
219            // Clear the ignoring region to handle further hover events.
220            mBoundsToIgnoreHoverEvent.setEmpty();
221            return;
222        }
223        super.performClickOn(key);
224    }
225
226    @Override
227    protected void onHoverEnterTo(final Key key) {
228        final int x = key.getHitBox().centerX();
229        final int y = key.getHitBox().centerY();
230        if (DEBUG_HOVER) {
231            Log.d(TAG, "onHoverEnterTo: key=" + key
232                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
233        }
234        mAccessibilityLongPressTimer.cancelLongPress();
235        if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
236            return;
237        }
238        // This hover enter event points to the key that isn't in the ignoring region.
239        // Further hover events should be handled.
240        mBoundsToIgnoreHoverEvent.setEmpty();
241        super.onHoverEnterTo(key);
242        if (key.isLongPressEnabled()) {
243            mAccessibilityLongPressTimer.startLongPress(key);
244        }
245    }
246
247    @Override
248    protected void onHoverExitFrom(final Key key) {
249        final int x = key.getHitBox().centerX();
250        final int y = key.getHitBox().centerY();
251        if (DEBUG_HOVER) {
252            Log.d(TAG, "onHoverExitFrom: key=" + key
253                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
254        }
255        mAccessibilityLongPressTimer.cancelLongPress();
256        super.onHoverExitFrom(key);
257    }
258
259    @Override
260    public void performLongClickOn(final Key key) {
261        if (DEBUG_HOVER) {
262            Log.d(TAG, "performLongClickOn: key=" + key);
263        }
264        final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID);
265        final long eventTime = SystemClock.uptimeMillis();
266        final int x = key.getHitBox().centerX();
267        final int y = key.getHitBox().centerY();
268        final MotionEvent downEvent = MotionEvent.obtain(
269                eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
270        // Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
271        tracker.processMotionEvent(downEvent, mKeyDetector);
272        // The above fake down event triggers an unnecessary long press timer that should be
273        // canceled.
274        tracker.cancelLongPressTimer();
275        downEvent.recycle();
276        // Invoke {@link MainKeyboardView#onLongPress(PointerTracker)} as if a long press timeout
277        // has passed.
278        mKeyboardView.onLongPress(tracker);
279        // If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
280        // or a key invokes IME switcher dialog, we should just ignore the next
281        // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
282        // {@link PointerTracker} is in operation or not.
283        if (tracker.isInOperation()) {
284            // This long press shows a more keys keyboard and further hover events should be
285            // handled.
286            mBoundsToIgnoreHoverEvent.setEmpty();
287            return;
288        }
289        // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
290        // We should ignore further hover events on this key.
291        mBoundsToIgnoreHoverEvent.set(key.getHitBox());
292        if (key.hasNoPanelAutoMoreKey()) {
293            // This long press has registered a code point without showing a more keys keyboard.
294            // We should talk back the code point if possible.
295            final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
296            final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
297                    mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
298            if (text != null) {
299                sendWindowStateChanged(text);
300            }
301        }
302    }
303}
304