• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.inputmethodservice.InputMethodService;
21 import android.support.v4.view.AccessibilityDelegateCompat;
22 import android.support.v4.view.ViewCompat;
23 import android.support.v4.view.accessibility.AccessibilityEventCompat;
24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
25 import android.util.SparseIntArray;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewParent;
29 import android.view.accessibility.AccessibilityEvent;
30 
31 import com.android.inputmethod.keyboard.Key;
32 import com.android.inputmethod.keyboard.Keyboard;
33 import com.android.inputmethod.keyboard.KeyboardId;
34 import com.android.inputmethod.keyboard.MainKeyboardView;
35 import com.android.inputmethod.keyboard.PointerTracker;
36 import com.android.inputmethod.latin.R;
37 
38 public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
39     private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();
40 
41     /** Map of keyboard modes to resource IDs. */
42     private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
43 
44     static {
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date)45         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time)46         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email)47         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im)48         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number)49         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone)50         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text)51         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time)52         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url)53         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url);
54     }
55 
56     private InputMethodService mInputMethod;
57     private MainKeyboardView mView;
58     private AccessibilityEntityProvider mAccessibilityNodeProvider;
59 
60     private Key mLastHoverKey = null;
61 
62     /**
63      * Inset in pixels to look for keys when the user's finger exits the keyboard area.
64      */
65     private int mEdgeSlop;
66 
67     /** The most recently set keyboard mode. */
68     private int mLastKeyboardMode;
69 
init(final InputMethodService inputMethod)70     public static void init(final InputMethodService inputMethod) {
71         sInstance.initInternal(inputMethod);
72     }
73 
getInstance()74     public static AccessibleKeyboardViewProxy getInstance() {
75         return sInstance;
76     }
77 
AccessibleKeyboardViewProxy()78     private AccessibleKeyboardViewProxy() {
79         // Not publicly instantiable.
80     }
81 
initInternal(final InputMethodService inputMethod)82     private void initInternal(final InputMethodService inputMethod) {
83         mInputMethod = inputMethod;
84         mEdgeSlop = inputMethod.getResources().getDimensionPixelSize(
85                 R.dimen.accessibility_edge_slop);
86     }
87 
88     /**
89      * Sets the view wrapped by this proxy.
90      *
91      * @param view The view to wrap.
92      */
setView(final MainKeyboardView view)93     public void setView(final MainKeyboardView view) {
94         if (view == null) {
95             // Ignore null views.
96             return;
97         }
98         mView = view;
99 
100         // Ensure that the view has an accessibility delegate.
101         ViewCompat.setAccessibilityDelegate(view, this);
102 
103         if (mAccessibilityNodeProvider == null) {
104             return;
105         }
106         mAccessibilityNodeProvider.setView(view);
107     }
108 
109     /**
110      * Called when the keyboard layout changes.
111      * <p>
112      * <b>Note:</b> This method will be called even if accessibility is not
113      * enabled.
114      */
setKeyboard()115     public void setKeyboard() {
116         if (mView == null) {
117             return;
118         }
119         if (mAccessibilityNodeProvider != null) {
120             mAccessibilityNodeProvider.setKeyboard();
121         }
122         final int keyboardMode = mView.getKeyboard().mId.mMode;
123 
124         // Since this method is called even when accessibility is off, make sure
125         // to check the state before announcing anything. Also, don't announce
126         // changes within the same mode.
127         if (AccessibilityUtils.getInstance().isAccessibilityEnabled()
128                 && (mLastKeyboardMode != keyboardMode)) {
129             announceKeyboardMode(keyboardMode);
130         }
131         mLastKeyboardMode = keyboardMode;
132     }
133 
134     /**
135      * Called when the keyboard is hidden and accessibility is enabled.
136      */
onHideWindow()137     public void onHideWindow() {
138         if (mView == null) {
139             return;
140         }
141         announceKeyboardHidden();
142         mLastKeyboardMode = -1;
143     }
144 
145     /**
146      * Announces which type of keyboard is being displayed. If the keyboard type
147      * is unknown, no announcement is made.
148      *
149      * @param mode The new keyboard mode.
150      */
announceKeyboardMode(int mode)151     private void announceKeyboardMode(int mode) {
152         final int resId = KEYBOARD_MODE_RES_IDS.get(mode);
153         if (resId == 0) {
154             return;
155         }
156         final Context context = mView.getContext();
157         final String keyboardMode = context.getString(resId);
158         final String text = context.getString(R.string.announce_keyboard_mode, keyboardMode);
159         sendWindowStateChanged(text);
160     }
161 
162     /**
163      * Announces that the keyboard has been hidden.
164      */
announceKeyboardHidden()165     private void announceKeyboardHidden() {
166         final Context context = mView.getContext();
167         final String text = context.getString(R.string.announce_keyboard_hidden);
168 
169         sendWindowStateChanged(text);
170     }
171 
172     /**
173      * Sends a window state change event with the specified text.
174      *
175      * @param text The text to send with the event.
176      */
sendWindowStateChanged(final String text)177     private void sendWindowStateChanged(final String text) {
178         final AccessibilityEvent stateChange = AccessibilityEvent.obtain(
179                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
180         mView.onInitializeAccessibilityEvent(stateChange);
181         stateChange.getText().add(text);
182         stateChange.setContentDescription(null);
183 
184         final ViewParent parent = mView.getParent();
185         if (parent != null) {
186             parent.requestSendAccessibilityEvent(mView, stateChange);
187         }
188     }
189 
190     /**
191      * Proxy method for View.getAccessibilityNodeProvider(). This method is called in SDK
192      * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
193      * node hierarchy provider.
194      *
195      * @param host The host view for the provider.
196      * @return The accessibility node provider for the current keyboard.
197      */
198     @Override
getAccessibilityNodeProvider(final View host)199     public AccessibilityEntityProvider getAccessibilityNodeProvider(final View host) {
200         if (mView == null) {
201             return null;
202         }
203         return getAccessibilityNodeProvider();
204     }
205 
206     /**
207      * Intercepts touch events before dispatch when touch exploration is turned on in ICS and
208      * higher.
209      *
210      * @param event The motion event being dispatched.
211      * @return {@code true} if the event is handled
212      */
dispatchTouchEvent(final MotionEvent event)213     public boolean dispatchTouchEvent(final MotionEvent event) {
214         // To avoid accidental key presses during touch exploration, always drop
215         // touch events generated by the user.
216         return false;
217     }
218 
219     /**
220      * Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
221      *
222      * @param event The hover event.
223      * @return {@code true} if the event is handled
224      */
dispatchHoverEvent(final MotionEvent event, final PointerTracker tracker)225     public boolean dispatchHoverEvent(final MotionEvent event, final PointerTracker tracker) {
226         if (mView == null) {
227             return false;
228         }
229 
230         final int x = (int) event.getX();
231         final int y = (int) event.getY();
232         final Key previousKey = mLastHoverKey;
233         final Key key;
234 
235         if (pointInView(x, y)) {
236             key = tracker.getKeyOn(x, y);
237         } else {
238             key = null;
239         }
240         mLastHoverKey = key;
241 
242         switch (event.getAction()) {
243         case MotionEvent.ACTION_HOVER_EXIT:
244             // Make sure we're not getting an EXIT event because the user slid
245             // off the keyboard area, then force a key press.
246             if (key != null) {
247                 getAccessibilityNodeProvider().simulateKeyPress(key);
248             }
249             //$FALL-THROUGH$
250         case MotionEvent.ACTION_HOVER_ENTER:
251             return onHoverKey(key, event);
252         case MotionEvent.ACTION_HOVER_MOVE:
253             if (key != previousKey) {
254                 return onTransitionKey(key, previousKey, event);
255             }
256             return onHoverKey(key, event);
257         }
258         return false;
259     }
260 
261     /**
262      * @return A lazily-instantiated node provider for this view proxy.
263      */
getAccessibilityNodeProvider()264     private AccessibilityEntityProvider getAccessibilityNodeProvider() {
265         // Instantiate the provide only when requested. Since the system
266         // will call this method multiple times it is a good practice to
267         // cache the provider instance.
268         if (mAccessibilityNodeProvider == null) {
269             mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod);
270         }
271         return mAccessibilityNodeProvider;
272     }
273 
274     /**
275      * Utility method to determine whether the given point, in local coordinates, is inside the
276      * view, where the area of the view is contracted by the edge slop factor.
277      *
278      * @param localX The local x-coordinate.
279      * @param localY The local y-coordinate.
280      */
pointInView(final int localX, final int localY)281     private boolean pointInView(final int localX, final int localY) {
282         return (localX >= mEdgeSlop) && (localY >= mEdgeSlop)
283                 && (localX < (mView.getWidth() - mEdgeSlop))
284                 && (localY < (mView.getHeight() - mEdgeSlop));
285     }
286 
287     /**
288      * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT on the previous key,
289      * a HOVER_ENTER on the current key, and a HOVER_MOVE on the current key.
290      *
291      * @param currentKey The currently hovered key.
292      * @param previousKey The previously hovered key.
293      * @param event The event that triggered the transition.
294      * @return {@code true} if the event was handled.
295      */
onTransitionKey(final Key currentKey, final Key previousKey, final MotionEvent event)296     private boolean onTransitionKey(final Key currentKey, final Key previousKey,
297             final MotionEvent event) {
298         final int savedAction = event.getAction();
299         event.setAction(MotionEvent.ACTION_HOVER_EXIT);
300         onHoverKey(previousKey, event);
301         event.setAction(MotionEvent.ACTION_HOVER_ENTER);
302         onHoverKey(currentKey, event);
303         event.setAction(MotionEvent.ACTION_HOVER_MOVE);
304         final boolean handled = onHoverKey(currentKey, event);
305         event.setAction(savedAction);
306         return handled;
307     }
308 
309     /**
310      * Handles a hover event on a key. If {@link Key} extended View, this would be analogous to
311      * calling View.onHoverEvent(MotionEvent).
312      *
313      * @param key The currently hovered key.
314      * @param event The hover event.
315      * @return {@code true} if the event was handled.
316      */
onHoverKey(final Key key, final MotionEvent event)317     private boolean onHoverKey(final Key key, final MotionEvent event) {
318         // Null keys can't receive events.
319         if (key == null) {
320             return false;
321         }
322         final AccessibilityEntityProvider provider = getAccessibilityNodeProvider();
323 
324         switch (event.getAction()) {
325         case MotionEvent.ACTION_HOVER_ENTER:
326             provider.sendAccessibilityEventForKey(
327                     key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
328             provider.performActionForKey(
329                     key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
330             break;
331         case MotionEvent.ACTION_HOVER_EXIT:
332             provider.sendAccessibilityEventForKey(
333                     key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
334             break;
335         }
336         return true;
337     }
338 
339     /**
340      * Notifies the user of changes in the keyboard shift state.
341      */
notifyShiftState()342     public void notifyShiftState() {
343         if (mView == null) {
344             return;
345         }
346 
347         final Keyboard keyboard = mView.getKeyboard();
348         final KeyboardId keyboardId = keyboard.mId;
349         final int elementId = keyboardId.mElementId;
350         final Context context = mView.getContext();
351         final CharSequence text;
352 
353         switch (elementId) {
354         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
355         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
356             text = context.getText(R.string.spoken_description_shiftmode_locked);
357             break;
358         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
359         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
360         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
361             text = context.getText(R.string.spoken_description_shiftmode_on);
362             break;
363         default:
364             text = context.getText(R.string.spoken_description_shiftmode_off);
365         }
366         AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
367     }
368 
369     /**
370      * Notifies the user of changes in the keyboard symbols state.
371      */
notifySymbolsState()372     public void notifySymbolsState() {
373         if (mView == null) {
374             return;
375         }
376 
377         final Keyboard keyboard = mView.getKeyboard();
378         final Context context = mView.getContext();
379         final KeyboardId keyboardId = keyboard.mId;
380         final int elementId = keyboardId.mElementId;
381         final int resId;
382 
383         switch (elementId) {
384         case KeyboardId.ELEMENT_ALPHABET:
385         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
386         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
387         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
388         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
389             resId = R.string.spoken_description_mode_alpha;
390             break;
391         case KeyboardId.ELEMENT_SYMBOLS:
392         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
393             resId = R.string.spoken_description_mode_symbol;
394             break;
395         case KeyboardId.ELEMENT_PHONE:
396             resId = R.string.spoken_description_mode_phone;
397             break;
398         case KeyboardId.ELEMENT_PHONE_SYMBOLS:
399             resId = R.string.spoken_description_mode_phone_shift;
400             break;
401         default:
402             resId = -1;
403         }
404 
405         if (resId < 0) {
406             return;
407         }
408         final String text = context.getString(resId);
409         AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
410     }
411 }
412