/* * 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.os.Bundle; 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.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.common.CoordinateUtils; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import java.util.List; /** * Exposes a virtual view sub-tree for {@link KeyboardView} and generates * {@link AccessibilityEvent}s for individual {@link Key}s. *

* 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. *

*/ final class KeyboardAccessibilityNodeProvider extends AccessibilityNodeProviderCompat { private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName(); // From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}. private static final int UNDEFINED = Integer.MAX_VALUE; private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper; private final AccessibilityUtils mAccessibilityUtils; /** 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 virtual view identifier for the hovering node. */ private int mHoveringNodeId = UNDEFINED; /** The keyboard view to provide an accessibility node info. */ private final KV mKeyboardView; /** The accessibility delegate. */ private final KeyboardAccessibilityDelegate mDelegate; /** The current keyboard. */ private Keyboard mKeyboard; public KeyboardAccessibilityNodeProvider(final KV keyboardView, final KeyboardAccessibilityDelegate delegate) { super(); mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance(); mAccessibilityUtils = AccessibilityUtils.getInstance(); mKeyboardView = keyboardView; mDelegate = delegate; // Since this class is constructed lazily, we might not get a subsequent // call to setKeyboard() and therefore need to call it now. setKeyboard(keyboardView.getKeyboard()); } /** * Sets the keyboard represented by this node provider. * * @param keyboard The keyboard that is being set to the keyboard view. */ public void setKeyboard(final Keyboard keyboard) { mKeyboard = keyboard; } private Key getKeyOf(final int virtualViewId) { if (mKeyboard == null) { return null; } final List sortedKeys = mKeyboard.getSortedKeys(); // Use a virtual view id as an index of the sorted keys list. if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) { return sortedKeys.get(virtualViewId); } return null; } private int getVirtualViewIdOf(final Key key) { if (mKeyboard == null) { return View.NO_ID; } final List sortedKeys = mKeyboard.getSortedKeys(); final int size = sortedKeys.size(); for (int index = 0; index < size; index++) { if (sortedKeys.get(index) == key) { // Use an index of the sorted keys list as a virtual view id. return index; } } return View.NO_ID; } /** * 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 = getVirtualViewIdOf(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 = AccessibilityEventCompat.asRecord(event); record.setSource(mKeyboardView, virtualViewId); return event; } public void onHoverEnterTo(final Key key) { final int id = getVirtualViewIdOf(key); if (id == View.NO_ID) { return; } // Start hovering on the key. Because our accessibility model is lift-to-type, we should // report the node info without click and long click actions to avoid unnecessary // announcements. mHoveringNodeId = id; // Invalidate the node info of the key. sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); } public void onHoverExitFrom(final Key key) { mHoveringNodeId = UNDEFINED; // Invalidate the node info of the key to be able to revert the change we have done // in {@link #onHoverEnterTo(Key)}. sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); } /** * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual * view, i.e. a descendant of the host View, with the given virtualViewId or * the host View itself if virtualViewId equals to {@link View#NO_ID}. *

* 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. *

*

* 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. *

* * @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); updateParentLocation(); // Add the virtual children of the root View. final List sortedKeys = mKeyboard.getSortedKeys(); final int size = sortedKeys.size(); for (int index = 0; index < size; index++) { final Key key = sortedKeys.get(index); if (key.isSpacer()) { continue; } // Use an index of the sorted keys list as a virtual view id. rootInfo.addChild(mKeyboardView, index); } return rootInfo; } // Find the key that corresponds to the given virtual view id. final Key key = getKeyOf(virtualViewId); if (key == null) { Log.e(TAG, "Invalid virtual view ID: " + virtualViewId); return null; } final String keyDescription = getKeyDescription(key); final Rect boundsInParent = key.getHitBox(); // 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.setEnabled(key.isEnabled()); info.setVisibleToUser(true); // Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key. // See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}. if (virtualViewId != mHoveringNodeId) { info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); if (key.isLongPressEnabled()) { info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); } } if (mAccessibilityFocusedView == virtualViewId) { info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } else { info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); } return info; } @Override public boolean performAction(final int virtualViewId, final int action, final Bundle arguments) { final Key key = getKeyOf(virtualViewId); if (key == null) { return false; } return performActionForKey(key, action); } /** * Performs the specified accessibility action for the given key. * * @param key The on which to perform the action. * @param action The action to perform. * @return The result of performing the action, or false if the action is not supported. */ boolean performActionForKey(final Key key, final int action) { switch (action) { case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: mAccessibilityFocusedView = getVirtualViewIdOf(key); sendAccessibilityEventForKey( key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); return true; case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: mAccessibilityFocusedView = UNDEFINED; sendAccessibilityEventForKey( key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); return true; case AccessibilityNodeInfoCompat.ACTION_CLICK: sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED); mDelegate.performClickOn(key); return true; case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); mDelegate.performLongClickOn(key); 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 = mKeyboard.mId.mEditorInfo; final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); final SettingsValues currentSettings = Settings.getInstance().getCurrent(); final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( mKeyboardView.getContext(), mKeyboard, key, shouldObscure); if (currentSettings.isWordSeparator(key.getCode())) { return mAccessibilityUtils.getAutoCorrectionDescription( keyCodeDescription, shouldObscure); } return keyCodeDescription; } /** * Updates the parent's on-screen location. */ private void updateParentLocation() { mKeyboardView.getLocationOnScreen(mParentLocation); } }