KeyboardAccessibilityDelegate.java revision adba09b54ed1b30bf9b24d632165229a0752b144
1/*
2 * Copyright (C) 2011 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.support.v4.view.AccessibilityDelegateCompat;
21import android.support.v4.view.ViewCompat;
22import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
23import android.util.Log;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.ViewParent;
27import android.view.accessibility.AccessibilityEvent;
28
29import com.android.inputmethod.keyboard.Key;
30import com.android.inputmethod.keyboard.KeyDetector;
31import com.android.inputmethod.keyboard.Keyboard;
32import com.android.inputmethod.keyboard.KeyboardView;
33
34/**
35 * This class represents a delegate that can be registered in a class that extends
36 * {@link KeyboardView} to enhance accessibility support via composition rather via inheritance.
37 *
38 * To implement accessibility mode, the target keyboard view has to:<p>
39 * - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
40 * - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
41 *
42 * @param  The keyboard view class type.
43 */
44public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
45        extends AccessibilityDelegateCompat {
46    private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
47    protected static final boolean DEBUG_HOVER = false;
48
49    protected final KV mKeyboardView;
50    protected final KeyDetector mKeyDetector;
51    private Keyboard mKeyboard;
52    private KeyboardAccessibilityNodeProvider mAccessibilityNodeProvider;
53    private Key mLastHoverKey;
54
55    public static final int HOVER_EVENT_POINTER_ID = 0;
56
57    public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
58        super();
59        mKeyboardView = keyboardView;
60        mKeyDetector = keyDetector;
61
62        // Ensure that the view has an accessibility delegate.
63        ViewCompat.setAccessibilityDelegate(keyboardView, this);
64    }
65
66    /**
67     * Called when the keyboard layout changes.
68     * <p>
69     * <b>Note:</b> This method will be called even if accessibility is not
70     * enabled.
71     * @param keyboard The keyboard that is being set to the wrapping view.
72     */
73    public void setKeyboard(final Keyboard keyboard) {
74        if (keyboard == null) {
75            return;
76        }
77        if (mAccessibilityNodeProvider != null) {
78            mAccessibilityNodeProvider.setKeyboard(keyboard);
79        }
80        mKeyboard = keyboard;
81    }
82
83    protected final Keyboard getKeyboard() {
84        return mKeyboard;
85    }
86
87    protected final void setLastHoverKey(final Key key) {
88        mLastHoverKey = key;
89    }
90
91    protected final Key getLastHoverKey() {
92        return mLastHoverKey;
93    }
94
95    /**
96     * Sends a window state change event with the specified string resource id.
97     *
98     * @param resId The string resource id of the text to send with the event.
99     */
100    protected void sendWindowStateChanged(final int resId) {
101        if (resId == 0) {
102            return;
103        }
104        final Context context = mKeyboardView.getContext();
105        sendWindowStateChanged(context.getString(resId));
106    }
107
108    /**
109     * Sends a window state change event with the specified text.
110     *
111     * @param text The text to send with the event.
112     */
113    protected void sendWindowStateChanged(final String text) {
114        final AccessibilityEvent stateChange = AccessibilityEvent.obtain(
115                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
116        mKeyboardView.onInitializeAccessibilityEvent(stateChange);
117        stateChange.getText().add(text);
118        stateChange.setContentDescription(null);
119
120        final ViewParent parent = mKeyboardView.getParent();
121        if (parent != null) {
122            parent.requestSendAccessibilityEvent(mKeyboardView, stateChange);
123        }
124    }
125
126    /**
127     * Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
128     * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
129     * node hierarchy provider.
130     *
131     * @param host The host view for the provider.
132     * @return The accessibility node provider for the current keyboard.
133     */
134    @Override
135    public KeyboardAccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
136        return getAccessibilityNodeProvider();
137    }
138
139    /**
140     * @return A lazily-instantiated node provider for this view delegate.
141     */
142    protected KeyboardAccessibilityNodeProvider getAccessibilityNodeProvider() {
143        // Instantiate the provide only when requested. Since the system
144        // will call this method multiple times it is a good practice to
145        // cache the provider instance.
146        if (mAccessibilityNodeProvider == null) {
147            mAccessibilityNodeProvider = new KeyboardAccessibilityNodeProvider(mKeyboardView);
148        }
149        return mAccessibilityNodeProvider;
150    }
151
152    /**
153     * Get a key that a hover event is on.
154     *
155     * @param event The hover event.
156     * @return key The key that the <code>event</code> is on.
157     */
158    protected final Key getHoverKeyOf(final MotionEvent event) {
159        final int actionIndex = event.getActionIndex();
160        final int x = (int)event.getX(actionIndex);
161        final int y = (int)event.getY(actionIndex);
162        return mKeyDetector.detectHitKey(x, y);
163    }
164
165    /**
166     * Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
167     *
168     * @param event The hover event.
169     * @return {@code true} if the event is handled.
170     */
171    public boolean onHoverEvent(final MotionEvent event) {
172        switch (event.getActionMasked()) {
173        case MotionEvent.ACTION_HOVER_ENTER:
174            onHoverEnter(event);
175            break;
176        case MotionEvent.ACTION_HOVER_MOVE:
177            onHoverMove(event);
178            break;
179        case MotionEvent.ACTION_HOVER_EXIT:
180            onHoverExit(event);
181            break;
182        default:
183            Log.w(getClass().getSimpleName(), "Unknown hover event: " + event);
184            break;
185        }
186        return true;
187    }
188
189    /**
190     * Process {@link MotionEvent#ACTION_HOVER_ENTER} event.
191     *
192     * @param event A hover enter event.
193     */
194    protected void onHoverEnter(final MotionEvent event) {
195        final Key key = getHoverKeyOf(event);
196        if (DEBUG_HOVER) {
197            Log.d(TAG, "onHoverEnter: key=" + key);
198        }
199        if (key != null) {
200            onHoverEnterTo(key);
201        }
202        setLastHoverKey(key);
203    }
204
205    /**
206     * Process {@link MotionEvent#ACTION_HOVER_MOVE} event.
207     *
208     * @param event A hover move event.
209     */
210    protected void onHoverMove(final MotionEvent event) {
211        final Key lastKey = getLastHoverKey();
212        final Key key = getHoverKeyOf(event);
213        if (key != lastKey) {
214            if (lastKey != null) {
215                onHoverExitFrom(lastKey);
216            }
217            if (key != null) {
218                onHoverEnterTo(key);
219            }
220        }
221        if (key != null) {
222            onHoverMoveWithin(key);
223        }
224        setLastHoverKey(key);
225    }
226
227    /**
228     * Process {@link MotionEvent#ACTION_HOVER_EXIT} event.
229     *
230     * @param event A hover exit event.
231     */
232    protected void onHoverExit(final MotionEvent event) {
233        final Key lastKey = getLastHoverKey();
234        if (DEBUG_HOVER) {
235            Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
236        }
237        if (lastKey != null) {
238            onHoverExitFrom(lastKey);
239        }
240        final Key key = getHoverKeyOf(event);
241        // Make sure we're not getting an EXIT event because the user slid
242        // off the keyboard area, then force a key press.
243        if (key != null) {
244            onRegisterHoverKey(key, event);
245            onHoverExitFrom(key);
246        }
247        setLastHoverKey(null);
248    }
249
250    /**
251     * Register a key that is selected by a hover event
252     *
253     * @param key A key to be registered.
254     * @param event A hover exit event that triggers key registering.
255     */
256    protected void onRegisterHoverKey(final Key key, final MotionEvent event) {
257        if (DEBUG_HOVER) {
258            Log.d(TAG, "onRegisterHoverKey: key=" + key);
259        }
260        simulateTouchEvent(MotionEvent.ACTION_DOWN, event);
261        simulateTouchEvent(MotionEvent.ACTION_UP, event);
262    }
263
264    /**
265     * Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}.
266     *
267     * @param touchAction The action of the synthesizing touch event.
268     * @param hoverEvent The base hover event from that the touch event is synthesized.
269     */
270    private void simulateTouchEvent(final int touchAction, final MotionEvent hoverEvent) {
271        final MotionEvent touchEvent = MotionEvent.obtain(hoverEvent);
272        touchEvent.setAction(touchAction);
273        mKeyboardView.onTouchEvent(touchEvent);
274        touchEvent.recycle();
275    }
276
277    /**
278     * Handles a hover enter event on a key.
279     *
280     * @param key The currently hovered key.
281     */
282    protected void onHoverEnterTo(final Key key) {
283        if (DEBUG_HOVER) {
284            Log.d(TAG, "onHoverEnterTo: key=" + key);
285        }
286        key.onPressed();
287        mKeyboardView.invalidateKey(key);
288        final KeyboardAccessibilityNodeProvider provider = getAccessibilityNodeProvider();
289        provider.onHoverEnterTo(key);
290        provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
291    }
292
293    /**
294     * Handles a hover move event on a key.
295     *
296     * @param key The currently hovered key.
297     */
298    protected void onHoverMoveWithin(final Key key) { }
299
300    /**
301     * Handles a hover exit event on a key.
302     *
303     * @param key The currently hovered key.
304     */
305    protected void onHoverExitFrom(final Key key) {
306        if (DEBUG_HOVER) {
307            Log.d(TAG, "onHoverExitFrom: key=" + key);
308        }
309        key.onReleased();
310        mKeyboardView.invalidateKey(key);
311        final KeyboardAccessibilityNodeProvider provider = getAccessibilityNodeProvider();
312        provider.onHoverExitFrom(key);
313    }
314}
315