| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.accessibility; |
| |
| import android.graphics.Rect; |
| import android.inputmethodservice.InputMethodService; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v4.view.accessibility.AccessibilityEventCompat; |
| import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; |
| import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; |
| import android.support.v4.view.accessibility.AccessibilityRecordCompat; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.inputmethod.EditorInfo; |
| |
| import com.android.inputmethod.keyboard.Key; |
| import com.android.inputmethod.keyboard.Keyboard; |
| import com.android.inputmethod.keyboard.KeyboardView; |
| import com.android.inputmethod.latin.CollectionUtils; |
| import com.android.inputmethod.latin.CoordinateUtils; |
| |
| /** |
| * Exposes a virtual view sub-tree for {@link KeyboardView} and generates |
| * {@link AccessibilityEvent}s for individual {@link Key}s. |
| * <p> |
| * A virtual sub-tree is composed of imaginary {@link View}s that are reported |
| * as a part of the view hierarchy for accessibility purposes. This enables |
| * custom views that draw complex content to report them selves as a tree of |
| * virtual views, thus conveying their logical structure. |
| * </p> |
| */ |
| public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat { |
| private static final String TAG = AccessibilityEntityProvider.class.getSimpleName(); |
| private static final int UNDEFINED = Integer.MIN_VALUE; |
| |
| private final InputMethodService mInputMethodService; |
| private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper; |
| private final AccessibilityUtils mAccessibilityUtils; |
| |
| /** A map of integer IDs to {@link Key}s. */ |
| private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray(); |
| |
| /** Temporary rect used to calculate in-screen bounds. */ |
| private final Rect mTempBoundsInScreen = new Rect(); |
| |
| /** The parent view's cached on-screen location. */ |
| private final int[] mParentLocation = CoordinateUtils.newInstance(); |
| |
| /** The virtual view identifier for the focused node. */ |
| private int mAccessibilityFocusedView = UNDEFINED; |
| |
| /** The current keyboard view. */ |
| private KeyboardView mKeyboardView; |
| |
| public AccessibilityEntityProvider(final KeyboardView keyboardView, |
| final InputMethodService inputMethod) { |
| mInputMethodService = inputMethod; |
| mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance(); |
| mAccessibilityUtils = AccessibilityUtils.getInstance(); |
| setView(keyboardView); |
| } |
| |
| /** |
| * Sets the keyboard view represented by this node provider. |
| * |
| * @param keyboardView The keyboard view to represent. |
| */ |
| public void setView(final KeyboardView keyboardView) { |
| mKeyboardView = keyboardView; |
| updateParentLocation(); |
| |
| // Since this class is constructed lazily, we might not get a subsequent |
| // call to setKeyboard() and therefore need to call it now. |
| setKeyboard(); |
| } |
| |
| /** |
| * Sets the keyboard represented by this node provider. |
| */ |
| public void setKeyboard() { |
| assignVirtualViewIds(); |
| } |
| |
| /** |
| * Creates and populates an {@link AccessibilityEvent} for the specified key |
| * and event type. |
| * |
| * @param key A key on the host keyboard view. |
| * @param eventType The event type to create. |
| * @return A populated {@link AccessibilityEvent} for the key. |
| * @see AccessibilityEvent |
| */ |
| public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) { |
| final int virtualViewId = generateVirtualViewIdForKey(key); |
| final String keyDescription = getKeyDescription(key); |
| final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); |
| event.setPackageName(mKeyboardView.getContext().getPackageName()); |
| event.setClassName(key.getClass().getName()); |
| event.setContentDescription(keyDescription); |
| event.setEnabled(true); |
| final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); |
| record.setSource(mKeyboardView, virtualViewId); |
| return event; |
| } |
| |
| /** |
| * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual |
| * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or |
| * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}. |
| * <p> |
| * A virtual descendant is an imaginary View that is reported as a part of |
| * the view hierarchy for accessibility purposes. This enables custom views |
| * that draw complex content to report them selves as a tree of virtual |
| * views, thus conveying their logical structure. |
| * </p> |
| * <p> |
| * The implementer is responsible for obtaining an accessibility node info |
| * from the pool of reusable instances and setting the desired properties of |
| * the node info before returning it. |
| * </p> |
| * |
| * @param virtualViewId A client defined virtual view id. |
| * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host |
| * View. |
| * @see AccessibilityNodeInfoCompat |
| */ |
| @Override |
| public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) { |
| if (virtualViewId == UNDEFINED) { |
| return null; |
| } |
| if (virtualViewId == View.NO_ID) { |
| // We are requested to create an AccessibilityNodeInfo describing |
| // this View, i.e. the root of the virtual sub-tree. |
| final AccessibilityNodeInfoCompat rootInfo = |
| AccessibilityNodeInfoCompat.obtain(mKeyboardView); |
| ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo); |
| |
| // Add the virtual children of the root View. |
| final Keyboard keyboard = mKeyboardView.getKeyboard(); |
| final Key[] keys = keyboard.mKeys; |
| for (Key key : keys) { |
| final int childVirtualViewId = generateVirtualViewIdForKey(key); |
| rootInfo.addChild(mKeyboardView, childVirtualViewId); |
| } |
| return rootInfo; |
| } |
| |
| // Find the view that corresponds to the given id. |
| final Key key = mVirtualViewIdToKey.get(virtualViewId); |
| if (key == null) { |
| Log.e(TAG, "Invalid virtual view ID: " + virtualViewId); |
| return null; |
| } |
| final String keyDescription = getKeyDescription(key); |
| final Rect boundsInParent = key.mHitBox; |
| |
| // Calculate the key's in-screen bounds. |
| mTempBoundsInScreen.set(boundsInParent); |
| mTempBoundsInScreen.offset( |
| CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation)); |
| final Rect boundsInScreen = mTempBoundsInScreen; |
| |
| // Obtain and initialize an AccessibilityNodeInfo with information about the virtual view. |
| final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); |
| info.setPackageName(mKeyboardView.getContext().getPackageName()); |
| info.setClassName(key.getClass().getName()); |
| info.setContentDescription(keyDescription); |
| info.setBoundsInParent(boundsInParent); |
| info.setBoundsInScreen(boundsInScreen); |
| info.setParent(mKeyboardView); |
| info.setSource(mKeyboardView, virtualViewId); |
| info.setBoundsInScreen(boundsInScreen); |
| info.setEnabled(true); |
| info.setVisibleToUser(true); |
| |
| if (mAccessibilityFocusedView == virtualViewId) { |
| info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); |
| } else { |
| info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); |
| } |
| return info; |
| } |
| |
| /** |
| * Simulates a key press by injecting touch events into the keyboard view. |
| * This avoids the complexity of trackers and listeners within the keyboard. |
| * |
| * @param key The key to press. |
| */ |
| void simulateKeyPress(final Key key) { |
| final int x = key.mHitBox.centerX(); |
| final int y = key.mHitBox.centerY(); |
| final long downTime = SystemClock.uptimeMillis(); |
| final MotionEvent downEvent = MotionEvent.obtain( |
| downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0); |
| final MotionEvent upEvent = MotionEvent.obtain( |
| downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0); |
| |
| mKeyboardView.onTouchEvent(downEvent); |
| mKeyboardView.onTouchEvent(upEvent); |
| downEvent.recycle(); |
| upEvent.recycle(); |
| } |
| |
| @Override |
| public boolean performAction(final int virtualViewId, final int action, |
| final Bundle arguments) { |
| final Key key = mVirtualViewIdToKey.get(virtualViewId); |
| if (key == null) { |
| return false; |
| } |
| return performActionForKey(key, action, arguments); |
| } |
| |
| /** |
| * Performs the specified accessibility action for the given key. |
| * |
| * @param key The on which to perform the action. |
| * @param action The action to perform. |
| * @param arguments The action's arguments. |
| * @return The result of performing the action, or false if the action is not supported. |
| */ |
| boolean performActionForKey(final Key key, final int action, final Bundle arguments) { |
| final int virtualViewId = generateVirtualViewIdForKey(key); |
| |
| switch (action) { |
| case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: |
| if (mAccessibilityFocusedView == virtualViewId) { |
| return false; |
| } |
| mAccessibilityFocusedView = virtualViewId; |
| sendAccessibilityEventForKey( |
| key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); |
| return true; |
| case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: |
| if (mAccessibilityFocusedView != virtualViewId) { |
| return false; |
| } |
| mAccessibilityFocusedView = UNDEFINED; |
| sendAccessibilityEventForKey( |
| key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Sends an accessibility event for the given {@link Key}. |
| * |
| * @param key The key that's sending the event. |
| * @param eventType The type of event to send. |
| */ |
| void sendAccessibilityEventForKey(final Key key, final int eventType) { |
| final AccessibilityEvent event = createAccessibilityEvent(key, eventType); |
| mAccessibilityUtils.requestSendAccessibilityEvent(event); |
| } |
| |
| /** |
| * Returns the context-specific description for a {@link Key}. |
| * |
| * @param key The key to describe. |
| * @return The context-specific description of the key. |
| */ |
| private String getKeyDescription(final Key key) { |
| final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); |
| final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); |
| final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( |
| mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure); |
| return keyDescription; |
| } |
| |
| /** |
| * Assigns virtual view IDs to keyboard keys and populates the related maps. |
| */ |
| private void assignVirtualViewIds() { |
| final Keyboard keyboard = mKeyboardView.getKeyboard(); |
| if (keyboard == null) { |
| return; |
| } |
| mVirtualViewIdToKey.clear(); |
| |
| final Key[] keys = keyboard.mKeys; |
| for (Key key : keys) { |
| final int virtualViewId = generateVirtualViewIdForKey(key); |
| mVirtualViewIdToKey.put(virtualViewId, key); |
| } |
| } |
| |
| /** |
| * Updates the parent's on-screen location. |
| */ |
| private void updateParentLocation() { |
| mKeyboardView.getLocationOnScreen(mParentLocation); |
| } |
| |
| /** |
| * Generates a virtual view identifier for the given key. Returned |
| * identifiers are valid until the next global layout state change. |
| * |
| * @param key The key to identify. |
| * @return A virtual view identifier. |
| */ |
| private static int generateVirtualViewIdForKey(final Key key) { |
| // The key x- and y-coordinates are stable between layout changes. |
| // Generate an identifier by bit-shifting the x-coordinate to the |
| // left-half of the integer and OR'ing with the y-coordinate. |
| return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY); |
| } |
| } |