1/*
2 * Copyright (C) 2012 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.graphics.Rect;
20import android.inputmethodservice.InputMethodService;
21import android.os.Bundle;
22import android.os.SystemClock;
23import android.support.v4.view.ViewCompat;
24import android.support.v4.view.accessibility.AccessibilityEventCompat;
25import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
26import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
27import android.support.v4.view.accessibility.AccessibilityRecordCompat;
28import android.util.Log;
29import android.util.SparseArray;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.inputmethod.EditorInfo;
34
35import com.android.inputmethod.keyboard.Key;
36import com.android.inputmethod.keyboard.Keyboard;
37import com.android.inputmethod.keyboard.KeyboardView;
38import com.android.inputmethod.latin.CollectionUtils;
39
40/**
41 * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
42 * {@link AccessibilityEvent}s for individual {@link Key}s.
43 * <p>
44 * A virtual sub-tree is composed of imaginary {@link View}s that are reported
45 * as a part of the view hierarchy for accessibility purposes. This enables
46 * custom views that draw complex content to report them selves as a tree of
47 * virtual views, thus conveying their logical structure.
48 * </p>
49 */
50public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
51    private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
52    private static final int UNDEFINED = Integer.MIN_VALUE;
53
54    private final InputMethodService mInputMethodService;
55    private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
56    private final AccessibilityUtils mAccessibilityUtils;
57
58    /** A map of integer IDs to {@link Key}s. */
59    private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray();
60
61    /** Temporary rect used to calculate in-screen bounds. */
62    private final Rect mTempBoundsInScreen = new Rect();
63
64    /** The parent view's cached on-screen location. */
65    private final int[] mParentLocation = new int[2];
66
67    /** The virtual view identifier for the focused node. */
68    private int mAccessibilityFocusedView = UNDEFINED;
69
70    /** The current keyboard view. */
71    private KeyboardView mKeyboardView;
72
73    public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
74        mInputMethodService = inputMethod;
75
76        mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
77        mAccessibilityUtils = AccessibilityUtils.getInstance();
78
79        setView(keyboardView);
80    }
81
82    /**
83     * Sets the keyboard view represented by this node provider.
84     *
85     * @param keyboardView The keyboard view to represent.
86     */
87    public void setView(KeyboardView keyboardView) {
88        mKeyboardView = keyboardView;
89        updateParentLocation();
90
91        // Since this class is constructed lazily, we might not get a subsequent
92        // call to setKeyboard() and therefore need to call it now.
93        setKeyboard(mKeyboardView.getKeyboard());
94    }
95
96    /**
97     * Sets the keyboard represented by this node provider.
98     *
99     * @param keyboard The keyboard to represent.
100     */
101    public void setKeyboard(Keyboard keyboard) {
102        assignVirtualViewIds();
103    }
104
105    /**
106     * Creates and populates an {@link AccessibilityEvent} for the specified key
107     * and event type.
108     *
109     * @param key A key on the host keyboard view.
110     * @param eventType The event type to create.
111     * @return A populated {@link AccessibilityEvent} for the key.
112     * @see AccessibilityEvent
113     */
114    public AccessibilityEvent createAccessibilityEvent(Key key, int eventType) {
115        final int virtualViewId = generateVirtualViewIdForKey(key);
116        final String keyDescription = getKeyDescription(key);
117
118        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
119        event.setPackageName(mKeyboardView.getContext().getPackageName());
120        event.setClassName(key.getClass().getName());
121        event.setContentDescription(keyDescription);
122        event.setEnabled(true);
123
124        final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
125        record.setSource(mKeyboardView, virtualViewId);
126
127        return event;
128    }
129
130    /**
131     * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
132     * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
133     * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
134     * <p>
135     * A virtual descendant is an imaginary View that is reported as a part of
136     * the view hierarchy for accessibility purposes. This enables custom views
137     * that draw complex content to report them selves as a tree of virtual
138     * views, thus conveying their logical structure.
139     * </p>
140     * <p>
141     * The implementer is responsible for obtaining an accessibility node info
142     * from the pool of reusable instances and setting the desired properties of
143     * the node info before returning it.
144     * </p>
145     *
146     * @param virtualViewId A client defined virtual view id.
147     * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual
148     *         descendant or the host View.
149     * @see AccessibilityNodeInfoCompat
150     */
151    @Override
152    public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
153        AccessibilityNodeInfoCompat info = null;
154
155        if (virtualViewId == UNDEFINED) {
156            return null;
157        } else  if (virtualViewId == View.NO_ID) {
158            // We are requested to create an AccessibilityNodeInfo describing
159            // this View, i.e. the root of the virtual sub-tree.
160            info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
161            ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, info);
162
163            // Add the virtual children of the root View.
164            final Keyboard keyboard = mKeyboardView.getKeyboard();
165            final Key[] keys = keyboard.mKeys;
166            for (Key key : keys) {
167                final int childVirtualViewId = generateVirtualViewIdForKey(key);
168                info.addChild(mKeyboardView, childVirtualViewId);
169            }
170        } else {
171            // Find the view that corresponds to the given id.
172            final Key key = mVirtualViewIdToKey.get(virtualViewId);
173            if (key == null) {
174                Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
175                return null;
176            }
177
178            final String keyDescription = getKeyDescription(key);
179            final Rect boundsInParent = key.mHitBox;
180
181            // Calculate the key's in-screen bounds.
182            mTempBoundsInScreen.set(boundsInParent);
183            mTempBoundsInScreen.offset(mParentLocation[0], mParentLocation[1]);
184
185            final Rect boundsInScreen = mTempBoundsInScreen;
186
187            // Obtain and initialize an AccessibilityNodeInfo with
188            // information about the virtual view.
189            info = AccessibilityNodeInfoCompat.obtain();
190            info.setPackageName(mKeyboardView.getContext().getPackageName());
191            info.setClassName(key.getClass().getName());
192            info.setContentDescription(keyDescription);
193            info.setBoundsInParent(boundsInParent);
194            info.setBoundsInScreen(boundsInScreen);
195            info.setParent(mKeyboardView);
196            info.setSource(mKeyboardView, virtualViewId);
197            info.setBoundsInScreen(boundsInScreen);
198            info.setEnabled(true);
199            info.setVisibleToUser(true);
200
201            if (mAccessibilityFocusedView == virtualViewId) {
202                info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
203            } else {
204                info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
205            }
206        }
207
208        return info;
209    }
210
211    /**
212     * Simulates a key press by injecting touch events into the keyboard view.
213     * This avoids the complexity of trackers and listeners within the keyboard.
214     *
215     * @param key The key to press.
216     */
217    void simulateKeyPress(Key key) {
218        final int x = key.mHitBox.centerX();
219        final int y = key.mHitBox.centerY();
220        final long downTime = SystemClock.uptimeMillis();
221        final MotionEvent downEvent = MotionEvent.obtain(
222                downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
223        final MotionEvent upEvent = MotionEvent.obtain(
224                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
225
226        mKeyboardView.onTouchEvent(downEvent);
227        mKeyboardView.onTouchEvent(upEvent);
228
229        downEvent.recycle();
230        upEvent.recycle();
231    }
232
233    @Override
234    public boolean performAction(int virtualViewId, int action, Bundle arguments) {
235        final Key key = mVirtualViewIdToKey.get(virtualViewId);
236
237        if (key == null) {
238            return false;
239        }
240
241        return performActionForKey(key, action, arguments);
242    }
243
244    /**
245     * Performs the specified accessibility action for the given key.
246     *
247     * @param key The on which to perform the action.
248     * @param action The action to perform.
249     * @param arguments The action's arguments.
250     * @return The result of performing the action, or false if the action is
251     *         not supported.
252     */
253    boolean performActionForKey(Key key, int action, Bundle arguments) {
254        final int virtualViewId = generateVirtualViewIdForKey(key);
255
256        switch (action) {
257        case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
258            if (mAccessibilityFocusedView == virtualViewId) {
259                return false;
260            }
261            mAccessibilityFocusedView = virtualViewId;
262            sendAccessibilityEventForKey(
263                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
264            return true;
265        case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
266            if (mAccessibilityFocusedView != virtualViewId) {
267                return false;
268            }
269            mAccessibilityFocusedView = UNDEFINED;
270            sendAccessibilityEventForKey(
271                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
272            return true;
273        }
274
275        return false;
276    }
277
278    /**
279     * Sends an accessibility event for the given {@link Key}.
280     *
281     * @param key The key that's sending the event.
282     * @param eventType The type of event to send.
283     */
284    void sendAccessibilityEventForKey(Key key, int eventType) {
285        final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
286        mAccessibilityUtils.requestSendAccessibilityEvent(event);
287    }
288
289    /**
290     * Returns the context-specific description for a {@link Key}.
291     *
292     * @param key The key to describe.
293     * @return The context-specific description of the key.
294     */
295    private String getKeyDescription(Key key) {
296        final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
297        final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
298        final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
299                mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure);
300
301        return keyDescription;
302    }
303
304    /**
305     * Assigns virtual view IDs to keyboard keys and populates the related maps.
306     */
307    private void assignVirtualViewIds() {
308        final Keyboard keyboard = mKeyboardView.getKeyboard();
309        if (keyboard == null) {
310            return;
311        }
312
313        mVirtualViewIdToKey.clear();
314
315        final Key[] keys = keyboard.mKeys;
316        for (Key key : keys) {
317            final int virtualViewId = generateVirtualViewIdForKey(key);
318            mVirtualViewIdToKey.put(virtualViewId, key);
319        }
320    }
321
322    /**
323     * Updates the parent's on-screen location.
324     */
325    private void updateParentLocation() {
326        mKeyboardView.getLocationOnScreen(mParentLocation);
327    }
328
329    /**
330     * Generates a virtual view identifier for the given key. Returned
331     * identifiers are valid until the next global layout state change.
332     *
333     * @param key The key to identify.
334     * @return A virtual view identifier.
335     */
336    private static int generateVirtualViewIdForKey(Key key) {
337        // The key x- and y-coordinates are stable between layout changes.
338        // Generate an identifier by bit-shifting the x-coordinate to the
339        // left-half of the integer and OR'ing with the y-coordinate.
340        return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY);
341    }
342}
343