futokb/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java
ryanlwlin 389e849a42 Implement text entry key API for accessibility services in AOSP Keyboard
From Android Q, Talkback supports lift-to-type feature if the node claims
it is a text entry key via setTextEntryKey(). We implement this API to
show how this API is applied.

This CL uses AccessibilityNodeInfoCompat instead of AccessibilityNodeInfo
so that the same functionality can be used even on pre-Q devices when
the AccessibilityService supports it.
With that, this CL removes the legacy code of lift-to-type feature,
which was implemented in the AOSP Keyboard side.

Bug: 131644969

Test: manual - enable Talkback suporting lift-to-type,check Talback
perform click action when finger is lifted.

Change-Id: I1ec2928f5a9ba0bde999b09d4c0b9c922f179a2a
2019-09-17 20:42:45 +08:00

340 lines
14 KiB
Java

/*
* 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 androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
import androidx.core.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.
* <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>
*/
final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView>
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<KV> mDelegate;
/** The current keyboard. */
private Keyboard mKeyboard;
public KeyboardAccessibilityNodeProvider(final KV keyboardView,
final KeyboardAccessibilityDelegate<KV> 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<Key> 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<Key> 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 <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);
updateParentLocation();
// Add the virtual children of the root View.
final List<Key> 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.setTextEntryKey(true);
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);
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);
}
}