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